【多线程】单例模式下的线程安全问题

1.单例模式下的线程安全

  • 单例模式的应用场景

某个类,不应该有多个实例,此时就可以使用单例模式(DataSource就是一个典型的案例,一一个程序中只有一个实例,不应该实例化多个DataSource对象)。如果尝试创建多个实例,编译期就会报错

  • 两种典型的单例模式
  1. 饿汉模式-线程安全

static修饰,在类加载的过程执行实例化,JVM保证了类加载的过程是线程安全的。

/**
 * 饿汉模式
 */
public class singlePattern {

    private static singlePattern instance =new singlePattern();

    //获取对象实例
    public static singlePattern getInstance(){
        return instance;
    }
    //私有构造方法
    private singlePattern(){}
}
  1. 懒汉模式-线程不安全

类被加载的时候,没有立刻被实例化,第一次调用getInstance的时候,才真正的实例化。

改进后的线程安全版本:

/**
* 懒汉模式
* 在需要对象的时候实例化
*/
public class lazyPattern {

   //当指令重排序的时候会出现问题,就需要对变量使用volatile修饰,保证可见性
   private volatile static lazyPattern instance =null;

   //synchronized修饰,线程安全
   public  static lazyPattern getInstance(){
       //第一次调用的时候,实例化
       if(instance ==null){
           //instance还没有初始化时,才会走到这个分支
           synchronized (lazyPattern.class){
               //加锁之后才能执行
               //第一个抢到锁的线程,看到instance 是null,进行初始化
               //保证 instance只会被实例化一次
               if (instance==null){
                   instance =new lazyPattern();
               }
           }
       }
       return instance;
   }

   private lazyPattern(){}
}

如果要是代码一整场都没有调用getInstance,此时实例化的过程也就被省略掉了,又称“延时加载”,一般认为“懒汉模式” 比 “饿汉模式”效率更高。懒汉模式有很大的可能是“实例用不到”,此时就节省了实例化的开销。

  1. volatile关键字
import java.util.concurrent.TimeUnit;

/**
 * volatile 演示示例
 * volatile 保证内存可见性
 */
public class volatileCase {

    volatile static boolean quit =false;

    static class MyThread extends Thread{

        @Override
        public void run() {
        long r =0;
        while (quit ==false){
            r++;
        }
            System.out.println(r);
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyThread thread =new MyThread();
        thread.start();

        TimeUnit.SECONDS.sleep(5);
        //在未使用volatile修饰quit变量时,此处的quit的修改,对于子线程来说是不可见的
        //所以,即使将quit改为true,对于thread线程来说仍是不可见的
        quit=true;
    }
}

主线程中对静态变量quit进行了修改,但是对于子线程Mythread来说,quit 的修改是不可见的,此时就需要使用关键字volatile修饰静态变量,保证内存的可见性,子线程中当quit被修改时就会输出变量r的值。
【多线程】单例模式下的线程安全问题_第1张图片

2.线程通知wait和notify

wait操作使线程进入阻塞,notify操作会唤醒阻塞中的线程,两者在使用的过程中都需要搭配synchronized使用。

public static void main(String[] args) throws InterruptedException {
        //锁
        Object o =new Object();
        synchronized (o){
         //1.wait()时,会释放锁   2.等待唤醒 3.唤醒后继续加锁
         o.wait();  
        //阻塞状态
         System.out.println("除非唤醒,否则永远不会到达!");
        }

o.wait()操作会使主线程进入阻塞,同时释放当前锁o,阻塞后的主线程会一直等待被唤醒,唤醒后会继续加锁。

创建一个子线程,唤醒主线程
    static class  MyThread extends Thread{
       private Object o;
       public MyThread(Object o){
           this.o=o;
       }
        @Override
        public void run() {
            synchronized (o){
                System.out.println("唤醒阻塞队列!!!");
                o.notify();
            }
        }
    }
在主线程中启动子线程:
public class Demo1 {
    static class  MyThread extends Thread{
       private Object o;
       public MyThread(Object o){
           this.o=o;
       }
        @Override
        public void run() {
            synchronized (o){
                System.out.println("唤醒阻塞队列!!!");
                o.notify();
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        //锁
        Object o =new Object();

        synchronized (o){
            //子线程唤醒
            MyThread myThread =new MyThread(o);
            myThread.start();
            //1.wait()时,会释放锁 2.等待唤醒 3.唤醒后继续加锁
            o.wait();   
            //阻塞状态
            System.out.println("除非唤醒,否则永远不会到达!");
        }
    }
}

此时,主线程被唤醒,打印语句就会执行:
【多线程】单例模式下的线程安全问题_第2张图片

3.生产-消费者模型-阻塞队列

阻塞队列(BlockingQueue) 是一个支持两个附加操作的队列。这两个附加的操作是:在队列为空时,获取元素的线程会等待队列变为非空。当队列满时,存储元素的线程会等待队列可用。阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

基于一对一生产-消费者模型下的阻塞队列:

**
 * 自定义阻塞队列
 * 生产-消费者模式
 * 一个生产者,一个消费者
 */
public class MyArrayBlockingQueue {
    //存储元素的数组
    private long[] array;
    //永远指向队列的第一个元素的索引位置
    private int frontIndex;
    //永远在队列的最后一个位置的下一个位置
    private int rearIndex;
    //队列元素的个数
    private int size;

    public MyArrayBlockingQueue(int capacity){
        array =new long[capacity];
        frontIndex=0;
        rearIndex=0;
        size=0;
    }
    /**
     * 入队操作
     * @param e
     * @throws InterruptedException
     */
    public synchronized void put(long e) throws InterruptedException {
        //先判断数组是否已满,避免假唤醒的情况,使用while
        while (array.length==size){
            //TODO:
            this.wait();
        }

        //预期队列一定不是满的
        array[rearIndex]=e;
        rearIndex++;
        //数组越界
        if (rearIndex ==array.length){
            rearIndex=0;
        }
        size++;
        //若有线程阻塞,一定是消费者等待
        notifyAll();
    }

    /**
     * 出队操作
     * @return
     * @throws InterruptedException
     */
    public synchronized long take() throws InterruptedException {
        while (size==0){
            //队列为空
            wait();
        }
        long e=array[frontIndex];
        frontIndex++;
        if (frontIndex==array.length){
            frontIndex=0;
        }
        size--;
        //若有线程阻塞,一定是生产者等待
        notifyAll();
        return e;
    }
}

一对一生产-消费者模式演示:

public class Test {
    static MyArrayBlockingQueue queue =new MyArrayBlockingQueue(3);

    static class MyThread extends Thread {
        @Override
        public void run() {
            Scanner sc =new Scanner(System.in);
            long e=sc.nextLong();

            //将元素入队
            try {
                queue.put(e);
            } catch (InterruptedException interruptedException) {
                interruptedException.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyThread thread =new MyThread();
        thread.start();

        //队列为空,主线程会阻塞,等待生产者生产
        long e=queue.take();
        System.out.println(e);
    }
}

主线程中,队列元素为空,消费者(主线程)发生阻塞,此时,主线程(消费者)唤醒生产者,即往队列中添加元素。子线程中,读取键盘输入往队列中添加元素,当队列中添加一个元素后,生产者唤醒消费者(主线程),即可取出队列元素。以上操作,就实现了一个简单的一对一的生产者-消费者模型。
【多线程】单例模式下的线程安全问题_第3张图片

你可能感兴趣的:(操作系统,单例模式,java,开发语言,单一职责原则)