单例设计模式:
单例设计模式:保证类在内存中只有一个对象。
怎样保证类在内存中只有一个对象?
- 构造方法私有,控制类的创建,不让其他类来创建本类的对象。
- 在本类中定义一个本类的对象。
- 对外提供公共的访问方式
单例的写法:
(1)饿汉式
class Singleton {
// 1.构造方法私有
private Singleton() {
}
// 2.定义一个本类的对象
private static Singleton singleton = new Singleton();
// 3.对外提供公共的访问方法
public static Singleton getInstance(){
return singleton;
}
}
优点:代码简单,程序启动对象就已经存在了获取单例对象较快
缺点:不管程序中有没有用到单例,都会创建一个对象,由于静态对象是在类加载时就需要生成,还会降低应用的启动速度。
适用范围:类功能简单,占内存小,调用频繁。
(2)懒汉式
class Singleton {
// 1.构造方法私有
private Singleton() {
}
// 2.定义一个本类的对象
private static Singleton singleton;
// 3.对外提供公共的访问方法
public static Singleton getInstance(){
if (singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
优点:单例对象的生成是在应用需要使用单例对象时才去构造,可以提高应用的启动速度
缺点:不是线程安全的,在需要时才创建对象获取对象较慢,若多个线程同时调用getInstance方法,那么可能会生成多个对象,违反单例设计模式的原则。
适用范围:类功能复杂,占内存大,单线程环境。
为了解决懒汉式的写法是线程不安全的缺点,我们最先想到的就是加锁,使用synchorinize关键字保证一次只有一个线程可以进入到临界区。
(3)懒汉式同步锁
class Singleton {
// 1.构造方法私有
private Singleton() {
}
// 2.定义一个本类的对象
private static Singleton singleton;
// 3.对外提供公共的访问方法
public static synchronized Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
懒汉式同步锁的写法可以保证在大多数情况下。可以保证单例的唯一性,但是对于效率会产生影响,synchronized为getInstance方法加锁,将会带来很大效率丢失。我们可以在 synchronized 语句之前,额外添加一次判空操作,来优化懒汉式同步锁方案带来的效率损失。
(4)双重判空
class Singleton {
// 1.构造方法私有
private Singleton() {
}
// 2.定义一个本类的对象
private static Singleton singleton;
// 3.对外提供公共的访问方法
public static Singleton getInstance() {
// 第一次判空,保证多线程只有第一次调用getInstance 的时候才会加锁初始化
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
双重判空方案很好的解决了懒汉式同步锁方案效率上的损失,比如在多个线程场景中,即使在第一次if (singleTon == null) 判空操作中让出 CPU 去执行,那么在另一个线程中也会在同步代码中初始化改单例对象,待 CPU 切换回来的时候,也会在第二次判空的时候得到正确结果。
我们都以为这样的方案已经很完美了,然而忽略了JVM的指令重排。
什么是指令重排?
简单来说指令重排就是代码运行时,JVM 并不一定总是按照我们想让它按照编码顺序去执行我们所想象的语义,它会在 “不改变” 原有代码语句含义的前提下进行代码,指令的重排序。
上述双重检验锁的单例实现问题主要出在哪里呢?问题出在 singleton = new Singleton();这句话在执行的过程。首先应该进行对象的创建操作大体可以分为三步:
1.分配内存空间。
2.初始化对象即执行构造方法。
3.设置 Instance 引用指向该内存空间。
如果有指令重排的前提下,这三部的执行顺序将有可能发生变化:
1.分配内存空间。
2.设置 Instance 引用指向该内存空间。
3.初始化对象即执行构造方法。
上述2和3重排了,但是在单线程环境下并不影响结果。但是在多线程下可能会出现。
线程A执行2.设置 Instance 引用指向该内存空间。此时singleton空的Instance,线程B执行第一个if判断返回false,得到了空的Instance。但是程序还未执行3.初始化对象即执行构造方法。所以线程B得到的是一个错误的资源。
为了解决这个问题我们应该避免程序重排,让 A 线程完成对象初始化后,B 再去判断singleton == null。
通过volatile避免指令重排
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1.保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2.禁止进行指令重排序。
(5) 双重校验锁
public class Singleton {
// 1.构造方法私有
private Singleton() {
}
// 2.定义一个本类的对象
private volatile static Singleton singleton;
// 3.对外提供公共的访问方法
public static Singleton getInstance() {
// 第一次判空,保证多线程只有第一次调用getInstance 的时候才会加锁初始化
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}