《java并发编程实战》-(2)-线程安全性-(安全发布)

(扯一段废话,第一次用markdown的方式写,只为更好的方便大家阅读。)
我们在面试的时候经常会被问到工作中都用到了哪些设计模式?或者阅读源码的时候都见过哪些设计模式的应用?手写一个单例的设计模式等等吧。在手写单例的时候我们一般会写懒汉模式,饿汉模式,内部类模式,枚举模式。在写懒汉模式中有一个双重检测机制的写法(不会的自行百度,这里就做过多的演示),这个写法有效的解决了安全的懒汉模式,synchronized关键字作用于方法上导致的效率低下(低下的原因是同一时刻只有一个线程能进入该方法,解决的方式就是将同步锁的作用域下放到代码块上)。但是这里我们要提一个大大的问号,这样的写法就是线程安全的了吗?这里涉及到一个知识点就是安全发布对象,且听我慢慢道来。

一、发布与逸出

1.1发布对象

定义:使一个对象能够被当前范围之外的代码所使用。
错误的发布对象例如:私有的成员变量赋过初值,对外通过公有成员方法发布这个成员变量,此时其它外部线程都可以访问到这个变量并进行修改。

1.2对象逸出

定义:一种错误的发布,当一个对象还没有构造完成时,就使他被其它线程所见。

public class Escape {

    private Integer thisCanBeEscape = 0;

    public Escape () {
        new InnerClass();
        thisCanBeEscape = null;
    }

    //内部类构造方法调用外部类的私有域
    private class InnerClass {

        public InnerClass() {
            log.info("{}", Escape.this.thisCanBeEscape);
        }
    }

    public static void main(String[] args) {
        new Escape();
    }
}

上述代码在函数构造过程中启动了一个线程。无论是隐式的启动还是显式的启动,都会造成这个this引用的溢出。新线程总会在所属对象构造完毕之前就已经看到它了。
因此要在构造函数中创建线程,那么不要启动它,而是应该采用一个专有的start或者初始化的方法统一启动线程。
这里其实我们可以采用工厂方法和私有构造函数来完成对象创建和监听器的注册等等,这样才可以避免错误。

如果不正确的发布对象会导致两种错误:
(1)发布线程意外的任何线程都可以看到被发布对象的过期的值
(2)线程看到的被发布线程的引用是最新的,然而被发布对象的状态却是过期的。

二、安全发布对象

1.在静态初始化函数中初始化一个对象引用
2.将对象的引用保存到volatile类型域或者AtomicReference对象中
3.将对象的引用保存到某个正确构造对象的final类型域中
4.将对象的引用保存到一个由锁保护的域中。

我们在回过头来看文章之初提出的问题,先把代码放出来。

public class SingletonExample {
    // 私有构造函数
    private SingletonExample() {
    }
    // 单例对象
    private static SingletonExample instance = null;
    // 静态的工厂方法
    public static SingletonExample getInstance() {
        if (instance == null) { // 双重检测机制
            synchronized (SingletonExample.class) { // 同步锁
                if (instance == null) {
                    instance = new SingletonExample();
                }
            }
        }
        return instance;
    }
}

在分析之前插播一个小知识点-----CPU指令重排
在上述代码中,执行new操作的时候,CPU一共进行了三次指令
(1)memory = allocate() 分配对象的内存空间
(2)ctorInstance() 初始化对象
(3)instance = memory 设置instance指向刚分配的内存

在程序运行过程中,CPU为提高运算速度会做出违背代码原有顺序的优化。我们称之为乱序执行优化或者说是指令重排。
那么上面知识点中的三步指令极有可能被优化为(1)(3)(2)的顺序(1的位置是不变的)。当我们有两个线程A与B,A线程遵从132的顺序,经过了两次instance的空值判断后,执行了new操作,并且cpu在某一瞬间刚结束指令(3),并且还没有执行指令(2)。而在此时线程B恰巧在进行第一次的instance空值判断,由于线程A执行完(3)指令,为instance分配了内存,线程B判断instance不为空,直接执行return,返回了instance,但此时其实并没有真正初始化好对象,(这种情况出现的概率还是比较低的)这样就出现了错误。

解决办法:
在对象声明时使用volatile关键字修饰,阻止CPU的指令重排。
private volatile static SingletonExample instance = null;
具体volatile是如何实现防止指令重排的,可以看java并发编程实战》-(2)-线程安全性中有关与volatile的相关内容。

你可能感兴趣的:(《java并发编程实战》-(2)-线程安全性-(安全发布))