单例设计模式是23种设计模式里面最简单的,但是要彻底理解单例,还是需要下一点功夫的。
单例一般会分为饿汉模式和懒汉模式
饿汉模式:
1 public class Singleton 2 { 3 private static Singleton singleton = new Singleton(); 4 5 public static Singleton getInstance() 6 { 7 return singleton; 8 } 9 }
但是在一些系统应用环境中,这个单例对象可能比较大,在类加载的时候就初始化对象会增加系统启动压力,还会对系统资源造成浪费。所以就有了懒汉模式,只有在第一次调用的时候才创界对象实例。
懒汉模式:
1 public class Singleton 2 { 3 private static Singleton singleton; 4 5 public static Singleton getInstance() 6 { 7 if (singleton == null) 8 { 9 singleton = new Singleton(); 10 } 11 return singleton; 12 } 13 }
但是在多线程的环境中,以上内容就会有问题了。由于没有同步,不同的线程可能同时进入if语句,然后分别创建了两个实例,这时候Singleton就不再是单列了。
于是就有了双重空判断版本
public class Singleton { private static Singleton1 singleton; public static Singleton1 getInstance() { if (singleton == null) { synchronized (Singleton1.class) { if (singleton == null) singleton = new Singleton(); } } return singleton; } }
网上很多资料说,这种写法也不是线程安全的,singleton字段必须定义为volatile才行。但实际上,以上代码其实并没有线程安全问题,因为Singleton1这个类并没有状态量。举个例子,以下代码才是非线程安全的:
public class WrongSingleton { private static WrongSingleton singleton; public Object state = new Object(); public static WrongSingleton getInstance() { if (singleton == null) { synchronized (WrongSingleton.class) { if (singleton == null) singleton = new WrongSingleton(); } } return singleton; } }
由于cpu指令重排序的存在,我们无法确认singleton对象引用和state对象引用回写到内存的顺序,如果Singleton对象的引用已经由线程A回写到了内存,而对象内部持有的state字段还未完成回写,那么此时线程B调用getInstance()方法后,将得到一个错误的Singleton对象。其state引用为null。
要解决这个问题有两个办法:
一、把 private static WrongSingleton singleton; 改为 private static volatile Singleton singleton; 由于volatile 的可见性语意,所有对volatile变量的修改,都会立即回写到主存,所以在Singleton构造函数返回前,state对象就已经回写到主存了。
二、把state 对象定义为final字段。由于final语意,final对象在构造函数完成后,其值的可见性对所有线程保持一致。