首页 > 代码库 > Java - 单例模式与多线程

Java - 单例模式与多线程

单例模式大家并不陌生,分为饿汉式和懒汉式等。

线程安全的饿汉式单例

饿汉式单例在类第一次加载的时候就完成了初始化,上代码:

public class MyObject {    private static MyObject myObject = new MyObject();    public static MyObject getInstance(){        return myObject;    }}

下面来验证饿汉式单例的线程安全性:

public class MyThread extends Thread{    public void run() {        System.out.println(MyObject.getInstance().hashCode());    }}public class Test {    public static void main(String[] args) throws Exception {        Thread t1 = new MyThread();        Thread t2 = new MyThread();        Thread t3 = new MyThread();        t1.start();        t2.start();        t3.start();    }}

输出:

763347431763347431763347431

三次输出 hashCode 是同一个值,说明饿汉式单例天生就是线程安全的。

结论:饿汉式单例在类第一次加载的时候完成初始化,而且是线程安全的。

 

非线程安全的懒汉式单例

懒汉式单例为延迟加载,调用 getInstance() 时才被创建,上代码:

public class MyObject {    private static MyObject myObject = null;
public static MyObject getInstance(){ if(myObject == null){ myObject = new MyObject(); } return myObject; }}

这种情况下肯定是非线程安全的。因为判断对象为空和创建对象是一个原子性操作,多线程访问产生竞态条件。(竞态条件:多线程并发中,正确的结果取决于多个线程的执行顺序)

下面做一下验证:

public class MyThread extends Thread{    public void run() {        System.out.println(MyObject.getInstance().hashCode());    }}public class Test {    public static void main(String[] args) throws Exception {        Thread t1 = new MyThread();        Thread t2 = new MyThread();        Thread t3 = new MyThread();        t1.start();        t2.start();        t3.start();    }}

输出:

21465096838104562281996534722

hashcode 输出不同,很明显懒汉式单例不具有线程安全性。

结论:懒汉式单例在需要该实例的时候才会进行初始化(仅初始化一次),但却是非线程安全的。在单例类数量少的情况下,这样一个延迟加载和饿汉式单例相比在性能上又没有明显的差距,所以我一般不会选择这种初级的懒汉式单例。

 

基于 volatile 的 DCL 方案为懒汉式单例提供线程安全性

在懒汉式单例的基础上,我们做一些优化使其具有线程安全的特性。

第一次尝试:

为 getInstance() 加上 synchronized 关键字

public class MyObject {    private static MyObject myObject = null;
synchronized public static MyObject getInstance(){ if(myObject == null){ myObject = new MyObject(); } return myObject; }}

输出

200054444520005444452000544445

得到的是单例,不过如果多个线程频繁调用 getInstance() 的话将会导致程序执行效率非常低下:下个线程想要取得对象锁,必须等到上一个线程释放锁之后才可以继续执行。

 

第二次尝试:

同步代码块

public class MyObject {    private static MyObject myObject = null;
public static MyObject getInstance() { synchronized (MyObject.class) { if (myObject == null) { myObject = new MyObject(); } } return myObject; }}

输出

512965639512965639512965639

然而这种方法等同于上一种尝试,唯一区别就是多线程下,上一次尝试中线程被堵在门口一米处进不来,而这一次尝试中线程被堵在门口半米处进不来。

 

第三次尝试:

继续缩小同步的代码范围

public class MyObject {    private static MyObject myObject = null;
public static MyObject getInstance() { if (myObject == null) {
synchronized (MyObject.class) { myObject = new MyObject(); } } return myObject; }}

输出

117575995620005444452146509683

相比前面几次尝试,执行效率会有显著提升,但是无法得到单例。因为破坏了原子性。

 

第四次尝试:

下面的代码使用了 DCL(双检查锁机制 double check lock )

public class MyObject {    private static MyObject myObject = null;
public static MyObject getInstance() { if (myObject == null) {
synchronized (MyObject.class) { if (myObject == null) { myObject = new MyObject(); } } } return myObject; }}

输出:

132019484913201948491320194849

然而此时就是线程安全了吗? 

No~ 不过我给不出反例推到,大家有经验的请指教一下~

 

第五次尝试:

我们还需要为“单例”加上 volatile 关键字。

public class MyObject {    volatile private static MyObject myObject = null;    public static MyObject getInstance() {        if (myObject == null) {            synchronized (MyObject.class) {                if (myObject == null) {                    myObject = new MyObject();                }            }        }        return myObject;    }}

此时才算是线程安全。

 

回头来看,“单例” 为何必须用 volatile 修饰呢 ?

因为关键字 volatile 能够禁止指令重排序。

Example - “单例” 在没有 volatile 修饰的情况下,线程A 和线程B 都是第一次调用该单例方法,线程A 先执行构造方法;然而该构造方法是一个非原子操作,编译后生成多条指令,由于指令重排序,可能会先执行赋值操作(实际是在内存中开辟一片存储对象的区域后直接返回内存的引用),之后 myObject 便不为空了,但是实际的初始化操作却还没有执行,如果就在此时线程B 进入,就会看到一个不为空的但是不完整(没有完成初始化)的对象。而 volatile 关键字能够禁止指令重排序优化,从而安全的实现单例。

 

结论:基于 volatile的 DCL方案能够保留延迟加载的特性,同时赋予了该单例线程安全性。

 

静态内部类实现单例模式

public class MyObject {    private static class Holder{        private static MyObject myObject = new MyObject();    }        public static MyObject getInstance() {        return Holder.myObject;    }}

怎样保证的线程安全? 会不会因为多个线程同时调用 getInstance() 出现指令重排导致初始化不完全呢?

JVM 在加载 class 之后就会进行类的初始化,在类的初始化期间,JVM 会去获取一个初始化锁,这个锁可以同步多个线程对同一个类的初始化,因此指令重排还是可能发生的,但是并不影响获得初始化锁的下一个线程,因为下一个线程进来的时候,上个线程已经完成了类的初始化。

从书上搞了个图过来:

技术分享

 

结论:静态内部类实现的单例模式能够保证线程安全,同时具有延迟加载特性。而且代码够简洁哦。

 

Java - 单例模式与多线程