在上一篇文章《从Java多线程可见性谈Happens-Before原则》中,我们详细讨论了在并发编程中Happens-Before原则对多线程共享变量的重要性。要想确保让一个线程对共享变量的修改能被其它线程感知到,就必须让两个线程中的操作满足Happens-Before原则。
在构建一个对象的过程中,更要考虑到多线程间共享数据的一致性问题,否则很可能会发生一个在A线程中构建完整的对象,在B线程中看到的却只被构建了一部分。例如下面的代码:

public class UnsafeLazyInitialization {
    private static Resource resource;
    
    public static Resource getInstance() {
        if(resource == null) {
            resource = new Resource();
        }
        return resource;
    }
}

上面的代码本意是想实现一个单例模式,但在多线程环境下,这个单例模式将很容易被打破。
首先,resource是一个普通变量,当一个线程更新resource变量的值时,其它线程可能无法感知到resource引用已经指向了一个创建好的对象,所以可能会导致程序创建多个Resource对象。
更糟糕的是重排序,在线程A中是先初始化Resource对象的各个field之后再将resource引用设置为指向它,线程B看到的可能是对引用变量resource的写入操作在对Resource对象各个field的写入操作之前发生。这会导致线程B看到的是一个被部分构造的Resource对象实例,该对象可能处于无效状态。
为了解决上面的问题,我们可以套用Happens-Before规则。例如在getInstance方法上声明synchronized。
然而对于构建一个对象实例的操作,除了可以使用Happens-Before原则外,我们利用下面两种方式也可以保证对象的构建状态被正确发布到其它线程。

静态初始化器

静态初始化器是由JVM在的初始化阶段执行,即在类被加载后并且被线程使用前。在静态初始化期间,内存写入操作将自动对所有线程可见。所以上面的程序改下成如下代码将会确保Resource对象被正确发布到其它线程:

public class EagerInitialization {
    private static Resource resource = new Resource();
    
    public static Resource getInstance() {
        return resource;
    }
}

含有final域的对象

对于含有final域的对象,初始化安全性可以防止对对象引用的写入操作被重排序到对象构造过程之前。在构造函数完成时,构造函数对final域的所有写入操作,以及通过final域可以到达的任何变量的写入操作,都能够被获取到该对象引用的线程看到。例如下面的代码:

public class FinalInitialization {
    private static final Resource resource;
    
    public static Resource getInstance() {
        if(resource == null) {
            resource = new Resource();
        }
        return resource;
    }
}

在上面的代码中,将resource变量声明为final类型的。这样可以保证,无论哪个线程,只要获取到对象的引用的值,就一定可以看到一个被完整构建的Resource对象。
但是此处的resource引用变量本身仍然需要其他机制保证可以对其它线程可见。所以final仅仅只能防止对对象引用的写入操作被重排序到对象构造完成之前。

双重校验锁单例模式

如果一定要使用懒加载形式的单例模式,可以采用双重校验锁的模式,代码如下:

public class SafeLazyInitialization {
    private volatile static Resource resource;
    
    public static Resource getInstance() {
        if(resource == null) {
            synchronized(SafeLazyInitialization.class) {
                if(resource == null) {
                    resource = new Resource();
                }
            }
        }
        return resource;
    }
}

相比于简单的在getInstance方法上添加synchronized,双重校验锁的模式并发度更好(只有当resource是null的时候才需要加锁,读取变量是并发执行的)。通过volatile和synchronized的配合使用确保resource变量在多个线程间的可见性。


poype
425 声望79 粉丝