今天正好看到InfoQ上边介绍的一则单例,就自己动手学习了一番,分享到博客。
首先,什么式单例模式呢? 单例模式,也叫单子模式,是一种常用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在(摘自维基百科),
然后单例分为饿汉式加载和懒汉式加载,
其实说的通俗一点就是,饿汉式是在类加载就是写在静态代码块里的,懒汉式是写在实例代码块中的(可能描述的不好,大家有什么好的解释欢迎吐槽),然后我们平常应该用的懒汉比较多,不要问为什么,或者你就在天天用饿汉式。
懒汉式:
1 public class Single1 {
2
3 private static Single1 instance;
4
5 public static Single1 getInstance() {
6
7 if (instance== null) {
8 instance= new Single1();
9 }
10 return instance;
11
12 }
13
14
15 }
这个就是我们很轻松就可以写出来的,当然有一点不好,没有显示Constructor,我们再加一个显示的私有构造,public肯定不能(看标题):
1 public class Single1 {
2
3 private static Single1 instance;
4
5 private Single1() {
6 }
7
8 public static Single1 getInstance() {
9
10 if (instance== null) {
11 instance = new Single1();
12 }
13 return instance;
14
15 }
16
17 }
这就是一个懒汉式单例,普通场景下已经满足我们的使用了,当然要是出现了线程并发执行时,基本上就行不通了,假如多线程情况下同时请求getInstance()方法,发现single1 == null ,全部返回一个新的实例,那么我们的单例就完全没有意义存在了, 所以接下来我们先来个加锁的单例:
1 public class Single1 {
2
3 private static Single1 instance;
4
5 private Single1() {
6 }
7
8 public static synchronized Single1 getInstance() {
9
10 if (instance== null) {
11 instance = new Single1();
12 }
13 return instance;
14
15 }
16 }
我们在单例的实现上加了synchronized关键字,如果多个线程请求该方法时,第一个线程会获得线程锁,其余线程则进入等待状态,等到锁释放才抢占执行。这样虽然解决了上面的问题,但是出现了新的问题,多线程情况下,请求该方法,非常浪费时间和资源,
所以我们就用到了双重检查
双重检查锁定模式(也被称为"双重检查加锁优化","锁暗示"(Lock hint)) 是一种软件设计模式用来减少并发系统中竞争和同步的开销。双重检查锁定模式首先验证锁定条件(第一次检查),只有通过锁定条件验证才真正的进行加锁逻辑并再次验证条件(第二次检查)。
它通常用于减少加锁开销,尤其是为多线程环境中的单例模式实现“惰性初始化”。惰性初始化的意思是直到第一次访问时才初始化它的值。
1 public class Single1 {
2
3 private static Single1 instance;
4
5 private Single1() {
6 }
7
8 public static Single1 getInstance() {
9
10 if (instance== null) {
11 synchronized(Single1.class) {
12 if (instance== null) {
13 instance = new Single1();
14 }
15 }
16
17 }
18 return instance;
19
20 }
21 }
第一个if检查是为了解决上面提到的多线程情况下的效率,第二种则是检查多个实例
虽然看起来已经符合我们的使用了,但是还要再了解下面几个概念
原子操作:
通俗说就是,操作不管处于任何状态,都必须执行完,不可分割。我们通常用到的赋值就是一个原子操作 m = 6; // 这是个原子操作
然而 int m = 6; 则不是,在计算机的处理过程中,这个操作被分成了2步:
• int m;
• m = 6;
所以,就会出现声明一个变量但是还没有被赋值的情况,例如有2个线程同时请求这个m,可能一个线程获取到了6,另一个才获取到了m的初始值,导致步稳定的情况发生
指令重排:
在计算机执行指令的顺序在经过程序编译器编译之后形成的指令序列,一般而言,这个指令序列是会输出确定的结果;以确保每一次的执行都有确定的结果。但是,一般情况下,CPU和编译器为了提升程序执行的效率,会按照一定的规则允许进行指令优化,在某些情况下,这种优化会带来一些执行的逻辑问题,主要的原因是代码逻辑之间是存在一定的先后顺序,在并发执行情况下,会发生二义性,即按照不同的执行逻辑,会得到不同的结果信息。
int a ; // 语句1
a = 8 ; // 语句2
int b = 9 ; // 语句3
int c = a + b ; // 语句4
由于指令重排的原因,上面的语句可能不是顺序执行,而是3412,2143来执行,对于上面的非原子操作,又会拆分开来执行,也就是说,对于非原子性的操作,在不影响最终结果的情况下,其拆分成的原子操作可能会被重新排列执行顺序。
看完了上面的两个概念,我们再来看上面的单例模式
instance= new Single1() 此语句并非原子操作,所以会被拆分,
1 给instance 分配 内存
2 调用Single1的构造函数初始化变量,形成实例
3 将当前对象指向内存地址
所以会出现instance 不为null,但是还没有指向正确的地址,其他线程过来读的时候发现不是null,可能直接返回了,造成问题。
private volatile static Single1 single1;
解决就是加一个volatile 关键字,强制不指令重排,instance声明为volatile之后,对它的写操作就会有一个内存屏障(什么是内存屏障?),这样,在它的赋值完成之前,就不用会调用读操作。volatile阻止的不singleton = new Singleton()这句话内部[1-2-3]的指令重排,而是保证了在一个写操作([1-2-3])完成之前,不会调用读操作(if (instance == null))
也就是说每个线程访问一个volatile作用域时会在继续执行之前读取它的当前值,而不是(可能)使用一个缓存的值。
•饿汉式
全局的单例实例在类装载时构建的实现方式
由于类装载的过程是由类加载器(ClassLoader)来执行的,这个过程也是由JVM来保证同步的,所以这种方式先天就有一个优势——能够免疫许多由多线程引起的问题。
1 public class Single1 {
2
3 private static Single1 SINGLE1 = new Single1();
4
5 private Single1() {
6 }
7
8 public static Single1 getInstance() {
9 return SINGLE1;
10
11 }
12 }
Effective Java 大神推荐的写法
1 // Effective Java 第一版推荐写法
2 public class Singleton {
3 private static class SingletonHolder {
4 private static final Singleton INSTANCE = new Singleton();
5 }
6 private Singleton (){}
7 public static final Singleton getInstance() {
8 return SingletonHolder.INSTANCE;
9 }
10 }
1 // Effective Java 第二版推荐写法
2 public enum SingleInstance {
3 INSTANCE;
4 public void fun1() {
5 // do something
6 }
7 }
8
9 // 使用
10 SingleInstance.INSTANCE.fun1();
本文参考自InfoQ