一、单例模式
单例模式,是一种设计模式.
单例=单个实例(对象)
某个类,在一个进程中,只应该创建出一个实例(原则上不应该有多个)
使用单例模式,就可以对代码进行一个更严格的校验和检查.
实现单例模式的方式有很多种,
此处介绍两种最基础的实现方式:
1. 饿汉模式
class Singleton{
private static Singleton instance = new Singleton();
public static Singleton getInstance(){
return instance;
}
}
这个instance,使用static创建,也就是说,instance就是Singleton类对象里面持有的属性.
这个引用,就是我们希望创建出的唯一的实例的引用.
Singleton.class (从.class文件加载到内存中,表示这个类的一个数据结构)
每个类的类对象,只存在一个.
类对象中的static属性,自然也是只有一个了.
因此,instance指向的这个对象,就是唯一uige对象.
其他代码想要使用这个类的实例,就需要通过这个方法来进行获取.
不应该在其他代码中重新new这个对象,而是使用这个方法获取到线程的对象.(创建好的对象,不是Thread)
private Singleton(){
}
当然,我们可以写上这样的一个构造方法,这样即使是想new也不行.
上述代码,就称为 " 饿汉模式 "
这是单例模式中一个非常简单的写法,所谓 " 饿 " 形容 " 非常迫切 "
实例在类加载的时候就创建了,创建的时机非常早,相当于程序一启动,实例就创建了.
ps :
使用反射可以打破上述约定.反射属于非常规手段.
2. 懒汉模式
创建实例的时机不太一样了.
创建实例的时机会更晚,只到第一次使用的时候,才会创建实例.
private static SingletonLazy instance = null;
public static SingletonLazy getInstance(){
if(instance == null){
instance = new SingletonLazy();
}
return instance;
}
如上述代码,懒汉模式,只有在第一次使用的时候才会创建实例.
如果是首次调用getInstance,那么此时,instance的引用为null,那么就会进入if循环,把实例创建出来.
如果是后续再次调用getInstance,由于instance已经不再是null,此时不会进入if,直接返回之前创建好的引用.
这样子仍可以保证,该类的实例是唯一一个了.
与此同时,创建实例的时机就不是程序启动时了,而是第一次调用getInstance的时候了.
二、单例模式的线程安全
那么接下来,我们来探讨这两个模式的线程安全情况.
1. 饿汉模式 -> 线程安全
对于饿汉模式来说,getInstance 直接返回 Instance 实例.这个操作本质上就是 " 读操作 ".
多个线程读取一个变量,线程是安全的.
2. 懒汉模式 ->线程不安全 ( 在多线程情况下可能会创建出多个实例 )
在懒汉模式中,代码有读也有写.
在代码的执行中,如果t1执行完if后,接着执行线程t2,由于此时还没有创建实例,所以instance依旧是null,此时t2就会创建一个实例,然后继续执行t1的if语句中的内容,就又创建了一个实例.
这就导致实例被new了两次,这就不是单例模式了.
那么.如何改进懒汉模式,让他称为线程安全的代码呢?
-> synchronized 加锁
我们可以把代码进行这样的改进
private static SingletonLazy instance = null;
public static Object locker = new Object();
public static SingletonLazy getInstance(){
if(instance == null) {
synchronized (locker) {
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
在原先的if循环外,我们使用了synchronized进行加锁
这个加锁要在if的外面,因为要使if和创建实例是原子的,不然还是有可能发生线程安全问题.
并且,在synchronized外又嵌套了一层if,这是为了在第一次创建实例后,如果还是每次都进行加锁,进程的效率就会变低.
加锁意味着可能产生阻塞,一旦阻塞,什么时候能解除就不知道了.
所以,在锁的外面又使用了一次if,在需要加锁的时候才加锁.
第一个 if 判定的是是否要加锁.
第二个 if 判定的是是否要创建对象.
这两个if的条件相同 -> 线程安全&执行效率 -> " 双重校验锁 "
三、指令重排序引起的线程安全问题
3.1 指令重排序 引起的 线程安全问题
但是.这个代码还是有一些问题 -> 指令重排序 引起的 线程安全问题
指令重排序,也是编译器优化的一种方式.
调整原有代码的执行顺序,保证逻辑不变的前提下,提高程序的效率.
那么 这一个代码
instance = new SingletonLazy(); -> 可以拆分为三个大的步骤.( 不是指令 )
1. 申请一段内存空间
2. 在这个内存上调用构造方法,创建出这个实例
3. 把这个内存地址赋值给Instance 引用变量
正常是123的,但编译器也会优化成132.
结果都是instance拿到这个内存地址,但是可能会出现没有调用构造方法创建实例就直接赋值地址.
这就会导致,使用这个实例里面的属性和方法会出现错误.因为此时的instance的属性是一个未初始化的, " 全 0 的值 ".
3.2 利用volatile解决线程安全问题
解决这个问题的核心思路,就是利用volatile
volatile有两个功能 :
1. 保证内存可见性,每次访问变量必须都要重新读取内存,而不会优化到寄存器/缓存中.
2. 禁止指令重排序,针对这个被volatile修饰的变量的读写操作相关指令,是不能被重排序的.
private volatile static SingletonLazy instance = null;