设计模式:不使用synchronized和lock,如何实现线程安全的单例?

用synchronized和lock版

不使用synchronized和lock,如何实现一个线程安全的单例?

回答最多的是静态内部类和枚举。很好,这两种确实可以实现。

枚举

public enum Singleton { 
 INSTANCE; 
 public void whateverMethod() { 
 } 
}

静态内部类

public class Singleton { 
 private static class SingletonHolder { 
 private static final Singleton INSTANCE = new Singleton(); 
 } 
 private Singleton (){} 
 public static final Singleton getInstance() { 
 return SingletonHolder.INSTANCE; 
 } 
}

还有人回答的很简单:饿汉。很好,这个也是对的。

饿汉

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

饿汉变种

public class Singleton { 
 private static class SingletonHolder { 
 private static final Singleton INSTANCE = new Singleton(); 
 } 
 private Singleton (){} 
 public static final Singleton getInstance() { 
 return SingletonHolder.INSTANCE; 
 } 
}

(更多单例实现方式见:单例模式的七种写法)

问:这几种实现单例的方式的真正的原理是什么呢?答:以上几种实现方式,都是借助了ClassLoader的线程安全机制。

先解释清楚为什么说都是借助了ClassLoader。

从后往前说,先说两个饿汉,其实都是通过定义静态的成员变量,以保证instance可以在类初始化的时候被实例化。那为啥让instance在类初始化的时候被实例化就能保证线程安全了呢?因为类的初始化是由ClassLoader完成的,这其实就是利用了ClassLoader的线程安全机制啊。

再说静态内部类,这种方式和两种饿汉方式只有细微差别,只是做法上稍微优雅一点。这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。。。但是,原理和饿汉一样。

最后说枚举,其实,如果把枚举类进行反序列化,你会发现他也是使用了static final来修饰每一个枚举项。(详情见:深度分析Java的枚举类型—-枚举的线程安全性及序列化问题)

至此,我们说清楚了,各位看官的回答都是利用了ClassLoader的线程安全机制。至于为什么ClassLoader加载类是线程安全的,这里可以先直接回答:ClassLoader的loadClass方法在加载类的时候使用了synchronized关键字。也正是因为这样, 除非被重写,这个方法默认在整个装载过程中都是同步的(线程安全的)。详见:技术进阶:深度分析Java的ClassLoader机制(源码级别)

~所以呢,这里可以说,大家的回答都只答对了一半。虽然没有显示使用synchronized和lock,但是还是间接的用到了!!!!

那么,话又说回来,这里再问一句:不使用synchronized和lock,如何实现一个线程安全的单例?

不用synchronized和lock版

上面这几种方法其实底层也都用到了synchronized,那么有没有什么办法可以不使用synchronized和lock,如何实现一个线程安全的单例?

答案是有的,那就是CAS。关于CAS,这里简单介绍一下:

CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

在JDK1.5 中新增java.util.concurrent(J.U.C)就是建立在CAS之上的。相对于对于synchronized这种阻塞算法,CAS是非阻塞算法的一种常见实现。所以J.U.C在性能上有了很大的提升。

借助CAS(AtomicReference)实现单例模式:

public class Singleton {
 private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>(); 
 private Singleton() {}
 public static Singleton getInstance() {
 for (;;) {
 Singleton singleton = INSTANCE.get();
 if (null != singleton) {
 return singleton;
 }
 singleton = new Singleton();
 if (INSTANCE.compareAndSet(null, singleton)) {
 return singleton;
 }
 }
 }
}

代码比较简单,稍微了解一下AtomicReference的原理就可以看得懂。不了解的建议去看下,了解下这些CAS的实现。

用CAS的好处在于不需要使用传统的锁机制来保证线程安全,CAS是一种基于忙等待的算法,依赖底层硬件的实现,相对于锁它没有线程切换和阻塞的额外消耗,可以支持较大的并行度。

CAS的一个重要缺点在于如果忙等待一直执行不成功(一直在死循环中),会对CPU造成较大的执行开销。

补充,关于CAS:

CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。(在 CAS 的一些特殊情况下将仅返回 CAS 是否成功,而不提取当前值。)CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。”这其实和乐观锁的冲突检查+数据更新的原理是一样的。

这里再强调一下,乐观锁是一种思想。CAS是这种思想的一种实现方式。

Java对CAS的支持

在JDK1.5 中新增java.util.concurrent(J.U.C)就是建立在CAS之上的。相对于对于synchronized这种阻塞算法,CAS是非阻塞算法的一种常见实现。所以J.U.C在性能上有了很大的提升。

我们以java.util.concurrent中的AtomicInteger为例,看一下在不使用锁的情况下是如何保证线程安全的。主要理解getAndIncrement方法,该方法的作用相当于 ++i 操作。

public class AtomicInteger extends Number implements java.io.Serializable { 
 private volatile int value; 
 public final int get() { 
 return value; 
 } 
 public final int getAndIncrement() { 
 for (;;) { 
 int current = get(); 
 int next = current + 1; 
 if (compareAndSet(current, next)) 
 return current; 
 } 
 } 
 public final boolean compareAndSet(int expect, int update) { 
 return unsafe.compareAndSwapInt(this, valueOffset, expect, update); 
 } 
}

在没有锁的机制下需要字段value要借助volatile原语,保证线程间的数据是可见的。

这样在获取变量的值的时候才能直接读取。然后来看看++i是怎么做到的。 getAndIncrement采用了CAS操作,每次从内存中读取数据然后将此数据和+1后的结果进行CAS操作,如果成功就返回结果,否则重试直到成功为止。而compareAndSet利用JNI来完成CPU指令的操作。

另外,关于锁,也可以参考本号的另一篇文章:图文并茂:深入点学习Java中的锁原理、锁优化、CAS、AQS等


相关推荐