目录
一.什么是单例模式
二.用static来创建单例模式
三.饿汉模式与懒汉模式
四.饿汉模式与懒汉模式的线程安全问题
五.New引发的指令重排序问题
六.小结
一.什么是单例模式
单例模式就是指某个类有且只有一个实例(instance)
这个是由需求决定的,有些需求场景就要求实例不能有多个,通过单例模式,相当于对'单个实例'做了一个更加严格的约束
单例模式本质上就是通过借助编程语言的特性,强行限制某个类,不能创建多个实例
二.用static来创建单例模式
static修饰的成员/属性就变成了类成员/类属性,当属性变成类属性的时候,就是'单个实例'了,例如:
public class Demo{
static String str = "";
}
更具体的说是类对象的属性,而类对象是通过JVM加载.class文件来的,此时类对象,其实在JVM当中也是'单例'
即JVM针对某个.class文件只会加载一次,也就只有一个类对象,类对象上面的成员(即static修饰的)也就只有一份了,在整个进程中都是独一无二的
class Singleton{
private static Singleton instance = new Singleton();
public static Singleton getInstance(){//该方法能让类外面获取到唯一的'实例'
return instance;
}
//把构造方法设置为私有的,此时在类的外面就无法继续 new 一个新的实例了
private Singleton(){
};
}
在该单例类外面获取到单例的正确做法
Singleton instance = Singleton.getInstance();
三.饿汉模式与懒汉模式
上面所写的代码实现单例模式的方式就叫做"饿汉模式" ,因为程序运行时用到了这个类就进行加载,这个实例在类加载阶段就创建好了,创建的时机非常早
还有另一种创建单例模式的方式叫做"懒汉模式",相比于"饿汉模式"创建实例的时机更迟,带来了更高的效率
class SingletonLazy{
private static SingletonLazy instance = null;
public static SingletonLazy getInstance(){
//首次调用getInstance才会触发,后续调用getInstance发现instance不为空了,就不会再创建实例,立即返回instance
if(instance == null){
instance = new SingletonLazy();
}
return instance;
}
private SingletonLazy(){
}
}
public class Demo2 {
public static void main(String[] args) {
SingletonLazy instance = SingletonLazy.getInstance();
}
}
如果后面没人调用getInstance,这样就把构造实例的过程给节省下来了,效率也就提升了
或者即使有代码后面调用getInstance,但是调用的时机就比较晚,创建实例的时机也就迟了,能够和其他的耗时操作岔开了(一般程序刚启动跑起来的时候,要初始化的东西比较多,系统资源比较紧张),而饿汉模式刚开始就创建实例,与其他所需要初始化的东西竞争,使系统资源雪上加霜
所以使用懒汉模式比使用饿汉模式创建单例模式更好
四.饿汉模式与懒汉模式的线程安全问题
虽然懒汉模式创建单例模式更好,但还要考虑线程安全问题,即多个线程并发运行的时候
饿汉模式属于线程安全,而懒汉模式属于线程不安全
在饿汉模式的 getInstance 方法里面并没有关于修改的操作,只涉及到读操作,所以饿汉模式是线程安全的
而懒汉模式的 getInstance 方法里面既涉及到读操作也涉及到修改操作,线程就不安全了
在有多个线程的时候,例如线程1 LOAD 完进行 CMP 发现 instance 为空就NEW一个新的实例,但线程二在线程1 LOAD 完后也 LOAD 然后线程1 CMP 完线程2也 CMP ,此时线程1还没进行修改操作,所以线程2也判断 instance 为null,然后也就NEW了一个实例,这样就会导致实例被创建出来了多份
如何才能让懒汉模式线程安全?
就是让以上的操作变成原子的,即加锁
public static SingletonLazy getInstance(){
synchronized (SingletonLazy.class){//在读之前加上一把锁
if(instance == null){
instance = new SingletonLazy();
}
}//修改完毕后解锁
return instance;
}
该线程不安全的时候只是在实例创建之前(首轮调用的时候)才会触发线程不安全的问题,在实例创建好之后if条件就进不去,就只变成读操作,但是仍然会频繁的加锁和解锁,就会产生很大的开销,可能涉及到用户态和内核态之间的切换,代价比较大
所以需要判断什么时候需要加锁,什么时候不需要加锁,
实例创建之前,线程是不安全的,就需要加锁,实例创建之后,线程是安全的,就不需要加锁
因此可以在外面加上一层判断条件
public static SingletonLazy getInstance(){
if(instance == null){//判断是否要加锁
synchronized (SingletonLazy.class){
if(instance == null){//判断是否要创建实例
instance = new SingletonLazy();
}
}
}
return instance;
}
因为两个判断之间隔着一个加锁操作,加锁可能导致锁竞争,锁竞争就会导致阻塞,不知道什么时候能够唤醒,所以第一个if和第二个if的结果可能是截然不同的,即第一个if成立了,第二个if不一定成立
五.New引发的指令重排序问题
new操作本质上分为3个步骤:
①申请内存,获取内存首地址
②调用构造方法,来初始化实例
③ 把内存的首地址赋值给instance引用
在单线程的情况下,步骤②和步骤③的执行的先后顺序没什么差别,就可能发生指令重排序,执行的顺序为①③②
在多线程的情况下,第一个线程的执行顺序为①③②,在步骤③执行结束之后,步骤②执行开始之前,第二个线程调用了getInstance方法,这个方法就会认为instance的值不为null,那么就直接返回 instance 实例,并对instance进行解引用操作(使用里面的属性/方法),但此时线程1的步骤②其实还没执行,instance的值还没有初始化仍然为空,线程2调用了就会出现问题
例如:老师布置一份作业说明天早上之前写完,你没写完,第二天早上以为不会上交,跟老师讲我写完了,老师突然说拿给他看看,你拿不出来,这不就坏事了,这个情况跟指令重排序带来的问题是一样的.
那么如何禁止发生指令重排序呢?就是使用volatile来禁止
private volatile static SingletonLazy instance = null;//volatile用来禁止指令重排序
就是直接给instance加上volatile禁止有关instance的操作发生指令重排序
六.小结
创建线程安全的单例模式有三个要注意的点:
1.加锁将读写操作变为原子的
2.双重if避免不必要的加锁解锁
3.加volatile防止有关实例的操作指令重排序