java 中关于singleton 的坑比较深,看过几篇double check的文章,但还是一知半解是很正常的

整合几篇文章,记一下

不安全的实现

public class Singleton {

    private Singleton() {
    }

    private static Singleton instance = null;

    public static Singleton getInstance() {
        if (Singleton.instance == null) {
            Singleton.instance = new Singleton();
        }
        return Singleton.instance;
    }
}

这懒汉式,只适合于单线程程序,多线程需要保护getInstance()方法,否则可能会产生多个Singleton对象实例

  • 怎么实现线程安全的单例模式?
  • 如果不执行修改对象的操作的情况下,单单执行一个读取操作,还有没有进行同步的必要?
  • 保证单例的线程安全使用synchronized会产生什么样的问题?
  • 不用synchronized还有什么方式保证线程安全?

安全的方法

方式 1 synchronized 方法

确保getInstance()一次只能被一个线程调,锁定整个方法

public class Singleton {

    private static Singleton instance = null;

    private Singleton() {
    }

    public static synchronized Singleton getInstance() {
        if (Singleton.instance == null) {
            Singleton.instance = new Singleton();
        }
        return Singleton.instance;
    }
}

锁定整个方法耗资源,此代码实际会产生多线程访问问题的只有

instance = new Singleton(); 

为降低 synchronized 块性能方面影响,如果只锁定这行?

public class Singleton {

    private static Singleton instance = null;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (Singleton.instance == null) {
            synchronized (Singleton.class) {
                Singleton.instance = new Singleton();
            }
        }
        return Singleton.instance;
    }
}

当两个线程并发进入第一次判断instance是否为空的if 语句内部

第一个线程执行new操作,第二个线程阻断,当第一个线程执行完毕之后,第二个线程没有进行判断就直接进行new操作,多实例化了,所以这样也不是安全的

为了避免第二次进入synchronized块没有进行非空判断的情况发生,添加第二次条件判断,引入了在java中更诡异的 double check问题

java 不能用的 double check?

Effecitve Java 指出在 Java 中双重检查模式通常并不适用

public class Singleton {

    private static Singleton instance = null;

    private Singleton() {

    }

    public static Singleton getInstance() {

        if (instance == null){                                     //1
            synchronized (Singleton.class) {        //2
                if (instance == null) {                           //3
                    instance = new Singleton();         //4
                }
            }
        }
        return instance;
    }
}

可见性问题?

线程1执行完第4步,释放锁

线程2获得锁后执行到第3步,由于可见性的原因,发现instance还是null,从而初始化了两次?

但不会存在这种情况,因为synchronized能保证线程1在释放锁之前会将对变量的修改刷新到主存当中,线程2拿到的值是最新的

实际存在的问题是无序性

第4步这个new操作是无序的,它可能会被编译成:

  • a. 先分配内存,让instance指向这块内存
  • b. 在内存中创建对象

然而需要意识到synchronized虽然是互斥的,但不代表一次就把整个过程执行完,它在中间是可能释放时间片的,时间片不是锁(这里没转过来,会耽误很多时间)

也就是说可能在a执行完后,时间片被释放,线程2执行到1,这时读到的instance是不是null呢?(标记1)

基于可见性,可能是null,也可能不是null

非常奇葩的是,这例子中,如果读到的是null,反而没问题了,接下来会等待锁,然后再次判断时不为null,最后返回单例

如果读到的不是null,那么坏了,按逻辑它就直接return instance了,这个instance还没执行构造参数,去做事情的话,很可能就崩溃了

volatile

public class Singleton {

    private volatile static Singleton instance = null;

    private Singleton() {

    }

    public static Singleton getInstance() {

        if (instance == null){                                  //1
            synchronized (Singleton.class) {    //2
                if (instance == null)                           //3
                    instance = new Singleton();     //4
            }
        }
        return instance;
    }

}

jdk1.4及之前,volatile不能保证new操作的有序性,但是它能保证可见性,因此标记1处,读到的不是null,导致了问题

1.5 volatile关键字,初始化就不能是:

  • a. 先分配内存,让instance指向这块内存
  • b. 在内存中创建对象

而应该是:

  • a.在内存中创建对象
  • b.让instance指向这个对象

这种形式,也就避免了无序性问题

在 Java 中双重检查模式无效的原因是在不同步的情况下引用类型不是线程安全的

除了 long 和 double 的基本类型,双重检查模式是适用 的。下面这段代码就是正确的

private int count;  
public int getCount(){  
  if (count == 0){   
    synchronized(this){   
      if (count == 0){  
        count = computeCount();  //一个耗时的计算  
      }     
    }    
  }  
  return count;  
}  

双重检查锁定失败的问题并不归咎于 jvm 实现 bug,而是 jvm 内存模型允许“无序写入会导致二次检查失败

懒汉式的lazy方式实现单例弯弯绕太多,单线程编程的情况下懒汉式单例实现没有任何问题

方式 2, no synchronized ,用 static

public class Singleton {

    private static Singleton instance = new Singleton();
    private Singleton() {
    }

    public static Singleton getInstance() {
        return Singleton.instance;
    }
}

更加灵巧,没有使用同步但保证了只有一个实例,还同时具有了Lazy的特性

方式3 Initialization on Demand Holder(IODH)

public class Singleton {  
    static class SingletonHolder {  
        static Singleton instance = new Singleton();  
    }  
      
    public static Singleton getInstance(){  
        return SingletonHolder.instance;  
    }  
}  

在ResourceFactory中加入了一个私有静态内部类ResourceHolder ,对外提供的接口是 getResource()方法,也就是只有在ResourceFactory .getResource()的时候,Resource对象才会被创建

这种写法的巧妙之处在于ResourceFactory 在使用的时候ResourceHolder 会被初始化,但是ResourceHolder 里面的resource并没有被创建,这里隐含了一个是static关键字的用法,使用static关键字修饰的变量只有在第一次使用的时候才会被初始化,而且一个类里面static的成员变量只会有一份,这样就保证了无论多少个线程同时访问,所拿到的Resource对象都是同一个

饿汉式的实现方式虽然貌似开销比较大,但是不会出现线程安全的问题,也是解决线程安全的单例实现的有效方式

至于ThreadLocal,还是应该由使用场景来决定

在《Java与模式》中,作者提出:“饿汉式单例类可以在Java语言实现,但不易在C++内实现,因为静态初始化在C++里没有固定的顺序,因而静态的instance变量的初始化与类的加载顺序没有保证,可能会出问题

这就是为什么GoF在提出单例类的概念时,举的例子是懒汉式的。他们的书影响之大,以致Java语言中单例类的例子也大多是懒汉式的

实际上,饿汉式单例类更符合Java语言本身的特点

在应用设计模式的同时,分析具体的使用场景来选择合适的实现方式是非常必要的

jvm 内存模型改进

Doug Lea 写道:“JSR133 Java 内存模型,将引用类型声明为 volatile,双重检查模式就可以工作了

1.4 前 out-of-order writes 不可用, 1.5 可用,即可保证多线程下的单例

private volatile Resource resource;  
public Resource getResource(){  
  if (resource == null){   
    synchronized(this){   
      if (resource==null){  
        resource = new Resource();    
      }     
    }    
  }  
  return resource;  
}  

还有种用Final的方法

https://zh.wikipedia.org/wiki/%E5%8F%8C%E9%87%8D%E6%A3%80%E6%9F%A5%E9%94%81%E5%AE%9A%E6%A8%A1%E5%BC%8F

用枚举

private static enum EnumSingleton{  
        INSTANCE;  
          
        private Singleton singleton;  
          
        //JVM会保证此方法绝对只调用一次  
        private EnumSingleton(){  
            singleton = new Singleton();  
        }  
          
        public Singleton getInstance(){  
            return singleton;  
        }  
    }  

http://www.cnblogs.com/dolphin0520/