多线程环境下的单例模式


单线程环境下的单例实现运行在多线程环境下会出现问题(volatile也只能保证可见性,并不能保证原子性)。

package com.prac;
import java.util.ArrayList;

//单线程下的单例实现
class Singleton{
    private volatile static Singleton instance;
    public static Singleton getInstance(){
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
public class TestSingletonOfMT{
    private static ArrayList<Singleton> list = new ArrayList<Singleton>();    
    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    Singleton singleton = Singleton.getInstance();
                    synchronized (list) {//保证add操作的原子性
                        list.add(singleton);
                    }
                }
            }).start();
        }
        //等待所有的子线程执行结束,尚有子线程在执行,就将主线程处于就绪等待
        while(Thread.activeCount()>1){
            Thread.yield();
        }
        System.out.println("list.size() = "+list.size());
        for (int i = 0; i < list.size(); i++) {
            for (int j = i+1; j < list.size(); j++) {
                if (!(list.get(i) == list.get(j))) {
                    System.out.println("list["+i+"]" +" != "+"list["+j+"]");
                    System.out.println("list["+i+"] = "+list.get(i));
                    System.out.println("list["+j+"] = "+list.get(j));
                }
            }
        }
        
    }

}

以上示例代码在我的运行环境下输出如下:

list.size() = 5
list[0] != list[2]
list[0] = com.prac.Singleton@525483cd
list[2] = com.prac.Singleton@2a9931f5
list[0] != list[3]
list[0] = com.prac.Singleton@525483cd
list[3] = com.prac.Singleton@2a9931f5
list[0] != list[4]
list[0] = com.prac.Singleton@525483cd
list[4] = com.prac.Singleton@2a9931f5
list[1] != list[2]
list[1] = com.prac.Singleton@525483cd
list[2] = com.prac.Singleton@2a9931f5
list[1] != list[3]
list[1] = com.prac.Singleton@525483cd
list[3] = com.prac.Singleton@2a9931f5
list[1] != list[4]
list[1] = com.prac.Singleton@525483cd
list[4] = com.prac.Singleton@2a9931f5

表明多个线程去获取单实例得到的却不是同一个对象,违背了单实例模式的初衷。其原因在于,如下代码不能保证原子性:

        if (instance == null) {
            instance = new Singleton();
        }

同步机制是一种可行的改进策略,采用synchronized代码块改进成如下实现方式。

//多线程下的单例实现
class Singleton{
    private volatile static Singleton instance;
    public static Singleton getInstance(){
        if (instance == null) {
            //这个地方有可能存在线程切换
            synchronized (Singleton.class) {
                //必须要再次进行null值检测,因为在synchronized代码块和第一处null值检测之间,可能会有线程切换。
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

这种实现称为双检锁(DCL:Double-Checked Locking)实现。与静态域直接初始化方式实现相比,能够起到懒加载的效果。

实际上双检锁实现出现过很多陷阱,最明显的就是指令重排序导致的双检锁失效问题:

instance = new Singleton();并非是原子操作,在指令级别层面对应了多条汇编指令,因此编译器和处理器有机会对其进行指令重排序。这就有可能导致在将引用变量instance指向堆内存中对象的存储区域时,Singleton对象并未真正初始化完成,也就是说此时instance引用到的对象是不可用、不完整的状态。

volatile修饰instance有以下两层语义:

1、保证instance变量的可见性(在汇编指令层面增加lock指令前缀)。

2、禁止指令重排序。

在JDK1.5中修复了volatile屏蔽指令重排序的语义后,搭配volatile关键字才能够安全的用DCL实现单例模式。JDK1.5之前仍然不能用volatile修复DCL失效的问题。

引文:



 

// double-checked-locking - don't do this!
private static Something instance = null;
public Something getInstance() {
  if (instance == null) {
    synchronized (this) {
      if (instance == null)
        instance = new Something();
    }
  }
  return instance;
}

The most obvious reason is that the writes which initialize instance and the write to the instance field can be reordered by the compiler or the cache, which would have the effect of returning what appears to be a partially constructed Something. The result would be that we read an uninitialized object. There are lots of other reasons why this is wrong, and why algorithmic corrections to it are wrong. There is no way to fix it using the old Java memory model.



 

另外一种相似的改进方式如下(或者直接用synchronized修饰getInstance()方法)。虽然也能够正确的工作,但是这样的实现效率偏低,因为每一个线程调用getInstance()方法是都会先加锁,之后才进行null值检测,其实除了第一次调用getInstance()方法获取单例对象时有加锁的必要性外,之后无需加锁,只需要进行null值检测即可,这也是DCL实现中用两次null检测的原因。

//多线程下的单例实现
class Singleton{
    private volatile static Singleton instance;
    public static Singleton getInstance(){
        synchronized (Singleton.class) {
            if (instance == null) {
                instance = new Singleton();
            }
        }
        return instance;
    }
}

另外一个线程安全的延迟初始化解决方案是利用类加载阶段的初始化来实现(被称为Initialization On Demand Holder idiom)

class Singleton{
    private static class LazyHolder {
        static final Singleton INSTANCE = new Singleton();
    }
    public static Singleton getInstance() {
        return LazyHolder.INSTANCE;//LazyHolder类被初始化
    }
}

 

如果摒弃懒加载的优势,可以更简单的用静态域直接初始化的方式实现:

class Singleton {
    static Singleton singleton = new Singleton();
}

 

参考资料:

1、https://www.cs.umd.edu/~pugh/java/memoryModel/DoubleCheckedLocking.html

2、https://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html#dcl

posted @ 2017-12-28 22:38  Qcer  阅读(475)  评论(0编辑  收藏  举报