[TOC]
单例模式,是一种比较简单的设计模式,也是属于创立型模式(提供一种创建对象的模式或者形式)。
要点:
- 1. 波及一个繁多的类,这个类来创立本人的对象(不能在其余中央重写创立办法,初始化类的时候创立或者提供公有的办法进行拜访或者创立,必须确保只有单个的对象被创立)。
- 2. 单例模式不肯定是线程不平安的。
-
3. 单例模式能够分为两种:懒汉模式 (在第一次应用类的时候才创立,能够了解为类加载的时候特地懒,要用的时候才去获取,要是没有就创立,因为是单例,所以只有第一次应用的时候没有,创立后就能够始终用同一个对象), 饿汉模式(在类加载的时候就曾经创立,能够了解为饿汉曾经饿得饥渴难耐,必定先把资源紧紧拽在本人手中,所以在类加载的时候就会先创立实例)
关键字:
- 单例:
singleton
- 实例:
instance
- 同步:
synchronized
- 单例:
饿汉模式
1. 公有属性
第一种 single
是public
,能够间接通过 Singleton
类名来拜访。
public class Singleton {
// 私有化构造方法,以避免外界应用该构造方法创立新的实例
private Singleton(){}
// 默认是 public,拜访能够间接通过 Singleton.instance 来拜访
static Singleton instance = new Singleton();}
2. 私有属性
第二种是用 private
润饰singleton
,那么就须要提供static
办法来拜访。
public class Singleton {private Singleton(){ }
// 应用 private 润饰,那么就须要提供 get 办法供外界拜访
private static Singleton instance = new Singleton();
// static 将办法归类所有,间接通过类名来拜访
public static Singleton getInstance(){return instance;.}
}
3. 懒加载
饿汉模式,这样的写法是没有问题的,不会有线程平安问题(类的 static
成员创立的时候默认是上锁的,不会同时被多个线程获取到),然而是有毛病的,因为 instance
的初始化是在类加载的时候就在进行的,所以类加载是由 ClassLoader
来实现的,那么初始化得比拟早益处是起初间接能够用,害处也就是节约了资源,要是只是个别类应用这样的办法,依赖的数据量比拟少,那么这样的办法也是一种比拟好的单例办法。
在单例模式中个别是调用 getInstance()
办法来触发类装载,以上的两种饿汉模式显然没有实现 lazyload
(集体了解是用的时候才触发类加载)
所以上面有一种饿汉模式的改进版,利用外部类实现懒加载。
这种形式Singleton 类
被加载了,然而 instance
也不肯定被初始化,要等到 SingletonHolder
被被动应用的时候,也就是显式调用 getInstance()
办法的时候,才会显式的装载 SingletonHolder
类,从而实例化instance
。这种办法应用类装载器保障了只有一个线程可能初始化instance
,那么也就保障了单例,并且实现了懒加载。
值得注意的是:动态外部类尽管保障了单例在多线程并发下的线程安全性,然而在遇到序列化对象时,默认的形式运行失去的后果就是多例的。
public class Singleton {private Singleton(){ }
// 外部类
private static class SingletonHolder{private static final Singleton instance = new Singleton();
}
// 对外提供的不容许重写的获取办法
public static final Singleton getInstance(){return SingletonHolder.instance;}
}
懒汉模式
最根底的代码(线程不平安):
public class Singleton {
private static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance() {if (instance == null) {instance = new Singleton();
}
return instance;
}
}
这种写法,是在每次获取实例 instance
的时候进行判断,如果没有那么就会 new
一个进去,否则就间接返回之前曾经存在的 instance
。然而这样的写法 不是线程平安的 ,当有多个线程都执行getInstance()
办法的时候,都判断是否等于 null 的时候,就会各自创立新的实例,这样就不能保障单例了。所以咱们就会想到同步锁,应用 synchronized 关键字:
加同步锁的代码(线程平安,效率不高)
public class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {synchronized(Singleton.class){if (instance == null)
instance = new Singleton();}
return instance;
}
}
这样的话,getInstance()
办法就会被锁上,当有两个线程同时拜访这个办法的时候,总会有一个线程先取得了同步锁,那么这个线程就能够执行上来,而另一个线程就必须期待,期待第一个线程执行完 getInstance()
办法之后,才能够执行。这段代码 是线程平安的 ,然而效率不高,因为如果有很多线程,那么就必须让所有的都期待正在拜访的线程,这样就会 大大降低了效率。那么咱们有一种思路就是,将锁呈现期待的概率再升高,也就是咱们所说的双重校验锁(双检锁)。
public class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {if (instance == null){synchronized(Singleton.class){if (instance == null)
instance = new Singleton();}
}
return instance;
}
}
1.第一个 if 判断,是为了升高锁的呈现概率,前一段代码,只有执行到同一个办法都会触发锁,而这里只有 singleton
为空的时候才会触发,第一个进入的线程会创建对象,等其余线程再进入时对象已创立就不会持续创立,如果对整个办法同步,所有获取单例的线程都要排队,效率就会升高。
2.第二个 if 判断是和之前的代码起一样的作用。
下面的代码看起来曾经像是没有问题了,事实上,还有有很小的概率呈现问题,那么咱们先来理解:原子操作 , 指令重排。
1. 原子操作
- 原子操作,能够了解为不可分割的操作,就是它曾经小到不能够再切分为多个操作进行,那么在计算机中要么它齐全执行了,要么它齐全没有执行,它不会存在执行到中间状态,能够了解为没有中间状态。比方:赋值语句就是一个原子操作:
n = 1; // 这是一个原子操作
假如 n 的值以前是 0,那么这个操作的背地就是要么执行胜利 n 等于 1,要么没有执行胜利 n 等于 0,不会存在中间状态,就算是并发的过程中也是一样的。
上面看一句 不是原子操作 的代码:
int n =1; // 不是原子操作
起因:这个语句中能够拆分为两个操作,1. 申明变量 n,2. 给变量赋值为 1,从中咱们能够看出有一种状态是 n 被申明后然而没有来得及赋值的状态,这样的状况,在并发中,如果多个线程同时应用 n,那么就会可能导致不稳固的后果。
2. 指令重排
所谓指令重排,就是计算机会对咱们代码进行优化,优化的过程中会在不影响最初后果的前提下,调整原子操作的程序。比方上面的代码:
int a ; // 语句 1
a = 1 ; // 语句 2
int b = 2 ; // 语句 3
int c = a + b ; // 语句 4
失常的状况,执行程序应该是 1234,然而理论有可能是 3124,或者 1324,这是因为语句 3 和 4 都没有原子性问题,那么就有可能被拆分成原子操作,而后重排.
原子操作以及指令重排的根本理解到这里完结,看回咱们的代码:
次要是
instance = new Singleton()
,依据咱们所说的,这个语句不是原子操作,那么就会被拆分,事实上 JVM(java 虚拟机)对这个语句做的操作:
- 1. 给 instance 调配了内存
- 2. 调用 Singleton 的构造函数初始化了一个成员变量,产生了实例,放在另一处内存空间中
- 3. 将 instance 对象指向调配的内存空间,执行完这一步才算真的实现了,instance 才不是 null。
在一个线程外面是没有问题的,那么在多个线程中,JVM 做了指令重排的优化就有可能导致问题,因为第二步和第三步的程序是不可能保障的,最终的执行程序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行结束、2 未执行之前,被线程二抢占了,这时 instance
曾经是 非 null
了(但却没有初始化),所以线程二会间接返回 instance
,而后应用,就会报空指针。
从更上一层来说,有一个线程是 instance 曾经不为 null 然而仍没有实现初始化 中间状态,这个时候有一个线程刚刚好执行到第一个 if(instance==null
), 这里失去的 instance
曾经不是 null
,而后他间接拿来用了,就会呈现谬误。
对于这个问题,咱们应用的计划是加上 volatile
关键字。
public class Singleton {
private static volatile Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {if (instance == null){synchronized(Singleton.class){if (instance == null)
instance = new Singleton();}
}
return instance;
}
}
volatile
的作用:禁止指令重排,把 instance
申明为 volatile
之后,这样,在它的赋值实现之前,就不会调用读操作。也就是在一个线程没有彻底实现instance = new Singleton()
; 之前,其余线程不可能去调用读操作。
- 下面的办法实现单例都是基于没有 简单序列化和反射 的时候,否则还是有可能有问题的,还有最初一种办法是应用枚举来实现单例,这个能够说的比拟理想化的单例模式,主动反对序列化机制,相对避免屡次实例化。
public enum Singleton {
INSTANCE;
public void doSomething() {}
}
以上最举荐枚举形式,当然当初计算机的资源还是比拟足够的,饿汉形式也是不错的,其中懒汉模式下,如果波及多线程的问题,也须要留神写法。
最初揭示一下,volatile
关键字,只禁止指令重排序,保障可见性(一个线程批改了变量,对任何其余线程来说都是立刻可见的,因为会立刻同步到主内存),然而不保障原子性。
【作者简介】:
秦怀,公众号【秦怀杂货店】作者,技术之路不在一时,山高水长,纵使迟缓,驰而不息。这个世界心愿所有都很快,更快,然而我心愿本人能走好每一步,写好每一篇文章,期待和你们一起交换。
此文章仅代表本人(本菜鸟)学习积攒记录,或者学习笔记,如有侵权,请分割作者核实删除。人无完人,文章也一样,文笔稚嫩,在下不才,勿喷,如果有谬误之处,还望指出,感激不尽~