Java 多线程 02

产生线程安全问题的原因:

线程的working memory是cpu的寄存器和高速缓存的抽象描述:现在的计算机,cpu在计算的时候,并不总是从内存读取数据,它的数据读取顺序优先级 是:寄存器-高速缓存-内存。线程耗费的是CPU,线程计算的时候,原始的数据来自内存,在计算过程中,有些数据可能被频繁读取,这些数据被存储在寄存器和高速缓存中,当线程计算完后,这些缓存的数据在适当的时候应该写回内存。当多个线程同时读写某个内存数据时,就会产生多线程并发问题

同步和异步

同步:多个线程共享内存,因此需要一个等待机制。多个需要同时访问某个对象的线程需要进入这个对象的等待池形成队列,并且对象有锁的机制。形象的来说,就是一堆人排队上厕所,有队列,有锁。

异步:每个线程都包含了运行时自身所需要的数据和方法,不必关心其他线程在干什么。

几种线程不安全的例子:

 买票的案例:

public class UnsafeTicket implements Runnable{

    private int ticketNum=15;
    private volatile boolean stop=false;

    @Override
    public void run() {
        while (!stop)
        {
            buy();
        }
    }

    public static void main(String[] args) {
        UnsafeTicket unsafeTicket = new UnsafeTicket();
        new Thread(unsafeTicket,"thread1").start();
        new Thread(unsafeTicket,"thread2").start();
        new Thread(unsafeTicket,"thread3").start();
    }

    private void buy()
    {
        if(ticketNum<=0)
        {
            System.out.println("票卖完了,现在还有" + ticketNum + "张票");
            stop=true;
            return;
        }
        else
        {
            System.out.println(Thread.currentThread().getName() + "拿到了第" + ticketNum + "张票");
            ticketNum--;
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

不安全的List

import java.util.ArrayList;
import java.util.List;

public class UnsafeList {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
               list.add(Thread.currentThread().getName());
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        }
        System.out.println(list.size());
    }
}

虽然有10000个线程进行了添加,但是最终输出结果List大小只有9133.这是因为线程不安全,有的线程添加时直接覆盖了其他线程已经添加的位置。

使用synchronized方法或者synchronized代码块实现同步

类似于  private synchronized void buy(){}

把需要同步的操作放到这个同步方法中,这个同步方法在同一时刻只有一个线程能访问。实际上这时锁住的是this这个对象。会大大影响效率,特别是方法体里面东西特别多的时候。

synchronized代码块则比较方便,锁住的是synObject

synchronized(synObject)

{

//操作synObject

}

注意锁不住Integer等对象:
https://blog.csdn.net/mononoke111/article/details/88742903

下面的例子只是示范一下同步代码块怎么用

public class UnsafeTicket implements Runnable{

    Num num= new Num(15);
    private volatile boolean stop=false;

    @Override
    public void run() {
        while (!stop)
        {
            buy();
        }
    }

    public static void main(String[] args) {
        UnsafeTicket unsafeTicket = new UnsafeTicket();
        new Thread(unsafeTicket,"thread1").start();
        new Thread(unsafeTicket,"thread2").start();
        new Thread(unsafeTicket,"thread3").start();
    }

    private void buy()
    {
        synchronized (num)
        {
            if(num.getTickNum()<=0)
            {
                System.out.println("票卖完了,现在还有" + num.getTickNum() + "张票");
                stop=true;
                return;
            }
            else
            {
                System.out.println(Thread.currentThread().getName() + "拿到了第" + num.getTickNum() + "张票");
                num.setTickNum(num.getTickNum()-1);
                try {
                    Thread.sleep(200);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

    }
}

class Num{
    private int tickNum;

    Num(int tickNum) {
        this.tickNum = tickNum;
    }

    public int getTickNum() {
        return tickNum;
    }

    public void setTickNum(int tickNum) {
        this.tickNum = tickNum;
    }
}

说到几种锁的分类,其实也不是很严格的分类,很多的时候是不同的使用思想:

https://zhuanlan.zhihu.com/p/147920568

死锁

多个线程各自占有一些资源,并且互相等待其他其他线程释放资源。即相互等待对方资源。

public class MakeUp extends Thread{
    public static final LipStick lipStick=new LipStick();
    public static final Mirror mirror=new Mirror();

    private int choice;
    private String name;

    public MakeUp(String name, int choice) {
        this.choice = choice;
        this.name = name;
    }


    @Override
    public void run() {
        if (choice==0)
        {
            synchronized (mirror)
            {
                System.out.println(this.name+"拿到了镜子");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lipStick)
                {
                    System.out.println(this.name+"拿到了口红");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }

        }
        else
        {
            synchronized (lipStick)
            {
                System.out.println(this.name+"拿到了口红");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (mirror)
                {
                    System.out.println(this.name+"拿到了镜子");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }

        }
    }

    public static void main(String[] args) {
        new MakeUp("thread1", 1).start();
        new MakeUp("thread1", 0).start();

    }




}

class LipStick{

}

class Mirror{

}

这个例子很好的解释了,抱着锁然后等待其他锁的例子。

这个例子中,分开就行了。

死锁的四个必要条件:

这也是解决死锁问题的四种方法:

1、破坏不剥夺条件:让对面的司机放弃了自己已有的资源。

2、破坏请求与保持条件:在自己需要的材料缺少时,主动放弃自己持有的资源,防止出现互相等待。

3、破坏循环等待条件:由于筷子指定了编号和获取规则,所以每个锁定状态都将按照顺序执行,于是便杜绝了环路等待条件。

4、破坏互斥条件:由于每次使用时都拷贝一份,所以一个资源可以被多个进程使用。

事实上,使用预先拷贝资源解决死锁问题的方案一般并不常用。这是由于拷贝的成本往往很大,并且影响效率。实际工作中较常采用的是第三种方案,通过控制加锁顺序解决死锁:

  • 加锁顺序:当多个线程需要相同的一些锁,但是按照不同的顺序加锁,死锁就很容易发生。如果能确保所有的线程都是按照相同的顺序获得锁,那么死锁就不会发生。当然这种方式需要你事先知道所有可能会用到的锁,然而总有些时候是无法预知的。

除此之外,我们还可以通过设置加锁时限或添加死锁检测避免死锁:

  • 加锁时限:加上一个超时时间,若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。但是如果有非常多的线程同一时间去竞争同一批资源,就算有超时和回退机制,还是可能会导致这些线程重复地尝试但却始终得不到锁。
  • 死锁检测:死锁检测即每当一个线程获得了锁,会在线程和锁相关的数据结构中( map 、 graph 等)将其记下。除此之外,每当有线程请求锁,也需要记录在这个数据结构中。死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。

其中,死锁检测最出名的算法是由艾兹格·迪杰斯特拉在 1965 年设计的银行家算法,通过记录系统中的资源向量、最大需求矩阵、分配矩阵、需求矩阵,以保证系统只在安全状态下进行资源分配,由此来避免死锁,对于面算法岗的同学一定要对其有所了解。

 

避免死锁可以概括成三种方法:

    • 固定加锁的顺序(针对锁顺序死锁,锁对象的hashCode进行排序,银行家算法)
    • 开放调用(针对对象之间协作造成的死锁)
    • 使用定时锁-->tryLock()
      • 如果等待获取锁时间超时,则抛出异常而不是一直等待!

 

可重入锁

显式定义锁,锁的是自己创建的Lock对象。

ReentrantLock可重入锁,实现了Lock接口,一般就用这个来锁了。

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class UnsafeTicket implements Runnable{

    private int num=15;
    private volatile boolean stop=false;
    private final Lock lock= new ReentrantLock();

    @Override
    public void run() {
        while (!stop)
        {
            buy();
        }
    }

    public static void main(String[] args) {
        UnsafeTicket unsafeTicket = new UnsafeTicket();
        new Thread(unsafeTicket,"thread1").start();
        new Thread(unsafeTicket,"thread2").start();
        new Thread(unsafeTicket,"thread3").start();
    }

    private void buy()
    {
        try {
            lock.lock();
            if(num>=0)
            {
                System.out.println(Thread.currentThread().getName() + "拿到了第" + num + "张票");
                num--;
                Thread.sleep(1000);
            }
            else
            {
                System.out.println("票卖完了");
                stop=true;
                return;
            }

        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        finally {
            lock.unlock();
        }
    }
}

线程池

https://zhuanlan.zhihu.com/p/132748927

https://zhuanlan.zhihu.com/p/123328822

线程通信

提到了生产者消费者模式

两种方式:管程法(缓存区法),信号灯法

原文地址:https://www.cnblogs.com/take-it-easy/p/14028900.html