多线程安全延迟初始化的二种方式

前言

最近在看并发编程艺术这本书,对看书的一些总结及个人理解。

多线程安全延迟初始化的二种方式_第1张图片
hashiqi.jpeg

在Java多线程程序中,有时候需要采用延迟初始化来降低初始化类和创建对象的开销。
静态内部类的实现方式,一般工作中使用这种方式较多,

public class Singletion {

    private static class InnerSingletion {
        private static Singletion single = new Singletion();
    }

    public static Singletion getInstance(){
        return InnerSingletion.single;
    }

    public static void main(String[] args) {
        Singletion sg1 = Singletion.getInstance();
        Singletion sg2 = Singletion.getInstance();

        System.out.println(sg1.hashCode());
        System.out.println(sg2.hashCode());
    }

}

还有一种是双重检查锁定方式:

/**
 * if(ds == null){
 * ds = new DubbleSingleton();
 * }
 * 为何这一步要加上为空判断?
 *
 * 因为如果在多线程下初始化时间蛮长,那么第一个if都会进入,此时静态代码块如果不加if为null判断
 * 会初始化多次,
 *
 */
public class DubbleSingleton {

    private static DubbleSingleton ds;

    public  static DubbleSingleton getDs(){
        if(ds == null){
            try {
                //模拟初始化对象的准备时间...
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //锁整个类,而不是锁对象
            synchronized (DubbleSingleton.class) {
                System.out.println(Thread.currentThread().getName());
                //双重校验,这一步也需要不然会初始化三次
                if(ds == null){
                    ds = new DubbleSingleton(); //1
                }
            }
        }
        return ds;
    }

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> System.out.println(DubbleSingleton.getDs().hashCode()),"t1");
        Thread t2 = new Thread(() -> System.out.println(DubbleSingleton.getDs().hashCode()),"t2");
        Thread t3 = new Thread(() -> System.out.println(DubbleSingleton.getDs().hashCode()),"t3");

        t1.start();
        t2.start();
        t3.start();
    }

}

实际在阅读并发编程艺术这本书的时候,发现上面的双重检查锁定还是会存在一些问题,1处代码在编译期间会分解成下面三段伪代码

memory = allocate();  // 1:分配对象的内存空间
ctorInstance(memory);  // 2:初始化对象
instance = memory;    // 3:设置instance指向刚分配的内存地址 

上面的第二步和第三会发生重排序,

memory = allocate();  // 1:分配对象的内存空间
instance = memory;    // 3:设置instance指向刚分配的内存地址,注意,此时对象还没有被初始化!
ctorInstance(memory);  // 2:初始化对象 

此时就会造成一个线程(A)实际已经进入初始化过程中了,但是另一个线程(B)在判断的时候还是看到该对象不为null直接返回A线程还没有初始化的对象,导致错误,直接返回正确的姿势就是如下所示:

public class DubbleSingleton {

    private volatile static DubbleSingleton ds; //1,加上volatile关键字修饰

    public  static DubbleSingleton getDs(){
        if(ds == null){
            try {
                //模拟初始化对象的准备时间
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //锁整个类,而不是锁对象
            synchronized (DubbleSingleton.class) {
                if(ds == null){
                    ds = new DubbleSingleton(); //2
                }
            }
        }
        return ds;
    }
}

1中的volatile修饰有哪些作用呢?
不允许2中编译时的第二步和第三步进行重排序,这样就实现了线程安全的延迟初始化。

你可能感兴趣的:(多线程安全延迟初始化的二种方式)