这节讲一个单例模式,通过多个源码的改进、讲解来进展,本文会结合JMM相关知识,讲单例模式讲透。
一. 单例模式的定义
保证只有一个实例存在的类就叫单例对象的类
二. 分类
1. 应用场景----多线程环境和单线程环境
------ 在一般情况下,其实单线程下,你随便怎么写这个单例都是没问题的;问题出在多线程的情况下。
2. 初始化时机---类装载时和实例第一次被创建时
------ 一般来说,要想资源最大化利用,我们只在实例被需要的时候创建。这两者还是有点差别的。这个就不细说了。
三. 源码解析
方式:实例第一次被创建时
1. 最简单的单例
public class SingleTon1 {
private static SingleTon1 INSTANCE;
public static SingleTon1 getInstance() {
if (INSTANCE == null) {
INSTANCE = new SingleTon1();
}
return INSTANCE;
}
}
评析:这是最简单的做法。
问题:
1:类SingleTon1是可以被外部访问的----这里要改成private
2. 改进1
public class SingleTon2 {
private static SingleTon2 INSTANCE;
private SingleTon2(){}
public static SingleTon2 getInstance() {
if (INSTANCE == null) {
INSTANCE = new SingleTon2();
}
return INSTANCE;
}
}
评析:这样外部类就不能通过SingleTon结构体直接实例化了。
问题:
1:在多线程模式下,INSTANCE == null的判断并没有被同步,INSTANCE可能被
多次实例化
3. 改进2
增加同步机制,先用个public class SingleTon3 {
private static SingleTon3 INSTANCE;
private SingleTon3(){}
public static synchronized SingleTon3 getInstance() {
if (INSTANCE == null) {
INSTANCE = new SingleTon3();
}
return INSTANCE;
}
}
评析:通过给getInstance添加关键字synchronized,让所有调用getInstance的都能依次
排队。
问题:
1:getInstance方法在添加synchronized方法后,所有线程都在同时调用时,只
能有一个在运行,这将很大的降低吞吐率。
4. 改进3
增加同步机制,先用个public class SingleTon4 {
private static SingleTon4 INSTANCE;
Private SingleTon4(){}
public static synchronized SingleTon4 getInstance() {
if (INSTANCE == null) {
synchronized(SingleTon4.class) {
INSTANCE = new SingleTon4();
}
}
return INSTANCE;
}
}
评析:将synchronized移到实例化SingleTon的时候再判断,减少了getInstance时的线
程堵塞。
问题:
1:有可能会有两个线程同时通过了INSTANCE == null的判断。还是有多个实例。
5. 改进4
增加同步机制,先用个public class SingleTon5 {
private static SingleTon5 INSTANCE;
Private SingleTon5(){}
public static SingleTon5 getInstance() {
if (INSTANCE == null) {
synchronized(SingleTon5.class) {
if(INSTANCE == null) {
INSTANCE = new SingleTon5();
}
}
}
return INSTANCE;
}
}
评析:二次判断INSTANCE,确保了INSTANCE只会有一个实例。
问题:
1:JMM模型中讲到两个概念,原子性,有序性。
知识点补充:
原子性:指的是操作原子性。
JVM的内存模型确保了几点,
一、所有变量储存在主内存
二、每条线程有自己的工作内存(分内存)
三、线程间无法访问对方的内存
四、线程间通信通过主内存完成
如下图:
讲几个范例
1. M = 1; --- 原子操作,M已经被开辟出来,剩下只有一个赋值操作
2. Int M = 1: --- 非原子操作,这里包含两个操作
① M先声明,被开辟出来
②将1赋值给M
3. M++; --- 非原子操作,M++等价于M=M+1;
这里包含了两个操作
① 先执行M+1
② 将M+1赋值给M
所以,原子性意味着 一条语句在JVM中也只需要一条指令就完成。
有序性:指的是指令按顺序操作,1234这样。
实际上,计算机为了提高执行效率,会对指令的执行 自动优化,
指令1234--->1423.
用范例说话
int M = 1;//#这里是两条指令
M = 5;//#这里是一条指令
M++;//#这里是两条指令
M--;//#这里是两条指令
那么,JVM会将这段代码拆成7条指令才执行,1234567,但是为了执行效率,可能会
变成231476,或者4536217
大概是这么个意思,当然操作执行的结果依旧是不变的。
6. 再看 改进4
public class SingleTon5 {
......
public static SingleTon5 getInstance() {
if (INSTANCE == null) {
synchronized(SingleTon5.class) {
if(INSTANCE == null) {
INSTANCE = new SingleTon5();
}
}
}
return INSTANCE;
}
}
评析:INSTANCE = new SingleTon5();并不具备原子性。而且有序性也无法得到保障。
INSTANCE可以分为3步
① INSTANCE分配内存,设置初始值null(这应该先new SingleTon5()完成)
②调用SingleTon5的结构体初始化
③将INSTANCE指向new SingleTon5对象分配的内存空间
那么有序性这一项,其指令执行可能是1-2-3或者1-3-2.
问题:
在多线程环境下,如果有线程刚好执行到if (INSTANCE == null),而如果是1-3-2,
那么INSTANCE已经不为null了,只是并未初始化其结构体,然后就有Fatal Exception了
7. 改进5
利用volatilepublic class SingleTon6 {
private static volatileSingleTon6 INSTANCE;
Private SingleTon6(){}
public static SingleTon6 getInstance() {
if (INSTANCE == null) {
synchronized(SingleTon6.class) {
if(INSTANCE == null) {
INSTANCE = new SingleTon6();
}
}
}
return INSTANCE;
}
}
评析:volatile的功效有2个。
①创建内存屏障(可以理解成工作内存的变量与主内存的变量会强制同步)
--- 原子性解决
②禁止 指令重排 ---有序性解决
方式:类装载时
1. 先看源码
public class SingleTonB {
private static final SingleTonB INSTANCE = new SingleTonB();
Private SingleTonB(){}
public static SingleTon6 getInstance() {
return INSTANCE;
}
}
评析:INSTANCE在类加载时就被ClassLoader初始化了。
问题:
1:初始化太早,资源浪费,而且拖慢启动
2:如果初始化里头的变量有依赖,可能会有其他问题,因为被依赖的数据还没初
始化呢
2.再看一个源码:利用内部类的创建时机
public class SingleTonC {
private static classSingleTonHolder {
private static final SingleTonC INSTANCE = new SingleTonC();
}
Private SingleTonC(){}
public static SingleTonC getInstance() {
return SingleTonHolder.INSTANCE;
}
}
评析:内部类的创建依赖外部类的创建时间,在类加载时,ClassLoader会去初始化
SingleTonC的变量,这时SingleTonC在SingleTonHolder里头才会被初始化。也顺
利达到了目的。
3.再看另一个源码:利用枚举机制
File:SingleTon.java
package enumSingleton;
public enum SingleTon {
INSTANCE;
private SingleTonD instance;
SingleTon() {
instance = new SingleTonD();
}
public SingleTonD getInstance() {
return instance;
}
}
File:SingleTonD.java
package enumSingleton;
public class SingleTonD {
SingleTonD(){
System.out.println("i am singtonD");
}
}
File:testEnumSingleTonMain.java
package enumSingleton;
public class testEnumSingleTonMain {
public static void main(String[] args) {
SingleTon.INSTANCE.getInstance();
SingleTon.INSTANCE.getInstance();
SingleTon.INSTANCE.getInstance();
SingleTon.INSTANCE.getInstance();
SingleTon.INSTANCE.getInstance();
SingleTon.INSTANCE.getInstance();
SingleTon.INSTANCE.getInstance();
SingleTon.INSTANCE.getInstance();
SingleTon.INSTANCE.getInstance();
}
}
输出结果:
i am singtonD
评析:每个枚举实例都是static final类型的,也就表明只能被初始化一次。只有在枚举
被访问时才会被实例化。这个结构相当简洁。注意观察,其实 枚举单例 跟 利用
内部类的创建时机 还是有殊途同归的意思在。