单例模式与线程安全问题
单例模式的主要作用,是保证在应用程序中一个类只会有一个实例存在。典型的应用场景,比如文件系统建立目录,或数据库连接都需要这样的单例。单例模式有以下几种常见的实现方式:
- 饿汉式
- 懒汉式(双检锁)
- 内部类实现式
- 枚举实现式
一、饿汉式
//饿汉式 class Singleton { private static Singleton instance = new Singleton();//保证只有一个实例 private Singleton() {} public static Singleton getInstance() { return instance; } } //饿汉式变体 class Singleton { private static Singleton instance; static{ instance = new Singleton();//保证只有一个实例 } private Singleton() {} public static Singleton getInstance() { return instance; } }
类被加载时就对instance进行实例化,单实例就被创建了。养兵千日,用兵一时,不管以后用不用的着,先创建再说,这相当于以空间换时间。
优点:写法简单,类装载时完成实例化。避免了线程安全问题。
缺点:无论单例是否使用到,都会一直占用内存空间。
二、(双检锁)懒汉式
懒汉式是当需要实例时才生成该实例。
class Singleton { private static Singleton instance; private Singleton() {} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
if条件是个竞态条件,会存在线程安全问题。存在这样一种情况:A线程if判空通过,但还未创建实例,所以instance==null。而此时来了个B线程,检测到instance==null,判空也通过。这样造成的结果就是A和B两个线程最终都会创建一个实例。所以,这样的懒汉式是非线程安全的。
非安全的改进
public class Singleton { private static Singleton singleton; private Singleton() {} public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { singleton = new Singleton(); } } return singleton; } }
这样的写法是否是线程安全的呢?答案是否定的,这种写法跟前一种写法实际上效果是一样的。假如一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时就会产生多个实例,所以这种改进毫无意义。
非安全的双检锁方式
那么我们继续改进。在同步代码块中再进行一次判空操作,这种方式叫做双重检查锁(DCL),简称双检锁。
public class Singleton { private static Singleton singleton; private Singleton() {} public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
这种方式看起来无比正确,如果你是这么想的话,那你一定是个小白。实际上这种写法依然存在线程安全问题。具体原因就是指令的重排序,内存不可见等。具体可参考:The "Double-Checked Locking is Broken" Declaration(翻译:可以不要再使用Double-Checked Locking了)
安全的双检锁方式
public class Singleton { //用volatile修饰 private static volatile Singleton singleton; private Singleton() {} public static Singleton getInstance() { if (singleton == null) { synchronized (Singleton.class) { if (singleton == null) { singleton = new Singleton(); } } } return singleton; } }
正是由于双检锁存在的安全问题,java5开始引入了volatile关键字。这里使用volatile来修饰单例变量,它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。
使用volatile也有缺陷,就是会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率可能并不高。
三、内部类实现方式
public class Singleton { private Singleton() {} //内部类 private static class SingletonInstance { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return SingletonInstance.INSTANCE; } }
这种方式跟饿汉式方式采用的机制类似,但又有不同。两者都是采用了类装载的机制来保证初始化实例时只有一个线程。不同的地方在饿汉式方式是只要Singleton类被装载就会实例化,没有Lazy-Loading的作用,而静态内部类方式在Singleton类被装载时并不会立即实例化,而是在需要实例化(getInstance)时,才会装载SingletonInstance类,从而完成Singleton的实例化。
类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
优点:延迟加载,线程安全,效率高。
四、枚举实现方式
public enum Singleton { INSTANCE; }
借助枚举来实现单例模式,不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象。枚举单例在实际项目开发中见的比较少,原因可能是枚举是在JDK1.5中才被加进去的。不过,我们公司项目中就用到了该写法。
优点:实现简单。
缺点:当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用new,可能会给其他开发人员造成困扰,特别是看不到源码的时候。
五、JDK中的单例
JDK中的Runtime类就是用饿汉式单例实现的。
public class Runtime { private static Runtime currentRuntime = new Runtime(); public static Runtime getRuntime() { return currentRuntime; } /** Don't let anyone else instantiate this class */ private Runtime() {}
}
参考博客: