一、了解单例模式
单例模式是校招中最常考的设计模式之,单例模式能保证某个类在程序中只存在唯一一份实例, 而不会创建出多个实例
这一点在很多场景上都需要. 比如 JDBC 中的 DataSource 实例就只需要一个
那什么是设计模式?
设计模式好比象棋中的 “棋谱”. 红方当头炮, 黑方马来跳. 针对红方的一些走法, 黑方应招的时候有
一些固定的套路. 按照套路来走局势就不会吃亏.软件开发中也有很多常见的 “问题场景”. 针对这些问题场景, 大佬们总结出了一些固定的套路. 按照这个套路来实现代码, 也不会吃亏
单例模式具体的实现方式, 分成 “饿汉” 和 “懒汉” 两种.
我们还可以回忆下 枚举实现线程安全的单例模式!!
二、饿汉模式
类加载的同时, 创建实例 ,用到这个类就会立即加载,比较着急地去创建实例
//饿汉模式:都是线程安全的
class Singleton {
//1、使用 static 创建一个实例,并且立即进行实例化.这个instance对应的实例就是该类的唯一实例
//类加载阶段就会直接创建实例(程序中用到该类就会立即加载)
private static Singleton instance = new Singleton();
//2、把构造方法设置为私有的,防止其他地方对 Singleton 进行实例化
private Singleton() {
}
//3、提供一个方法,让外面能够拿到唯一实例
public static Singleton getinstance() {
return instance;
}
}
注意:
- 饿汉模式都是线程安全的,其中getinstance仅仅是读取了变量的内容,当多个线程只是读取一个变量的时候也是线程安全的
- 使用 static 创建一个实例,并立即进行实例化.这个instance对应的实例就是该类的唯一实例
为什么使用static创建实例就能保证是唯一实例呢?
因为static修饰的成员是"类成员",一个java程序中,jvm会保证只有一个类对象,进一步在饿汉模式中也就保证了类的static成员也是只有一份
- 把构造方法设置为私有的,防止其他地方对 Singleton 进行实例化。私有的构造方法无法在本类外部使用,也就导致本类无法用new实例化,这样可以控制对象的生成。
三、懒汉模式
3.1 多线程版本(线程不安全)
class Singleton1 {
//1、不是立即就初始化实例
private static Singleton1 instance = null;
//2、将构造方法设置为私有的
private Singleton1() {
}
//3、提供一个方法来获取实例.只有当我们真正用到这个实例的时候才会去创建这个 实例
public static Singleton1 getInstance() {
if (instance == null) {
instance = new Singleton1();
}
return instance;
}
}
注意:
在懒汉模式中,getinstance既包含了读,又包含了修改。而且读和修改还不是原子的,所以并不是线程安全的,因此也会创建出多个实例
上述线程不安全具体指的是在多线程的环境下,并发的调用getinstance如下:
初始的instance是null,加载到CPU中与if条件中的null进行比较,满足相等就会创建出一个实例。由于线程的抢占式执行,导致进行了两次比较且相等,于是就创建出了两个实例。
那如何解决该问题呢?加上 synchronized 可以改善这里的线程安全问题.
这里是针对Singleton类对象进行加锁,这个类对象在程序中只有一份,在多个线程调 getinstance的时候就可以保证是针对同一个对象加锁!
3.2 多线程版本改进(线程安全)
对于上面的问题,我们对其进行加锁后达到了线程安全。但是又出现了新的问题,如下:
对于前面懒汉模式的代码来说,线程不安全发生在instance被初始化之前,由于多线程调用 getinstance ,就可能同时涉及到读和修改操作。但是一旦instance被初始化之后,就只剩下了读操作,此时也就线程安全了。
所以按照上述加锁方式,无论代码是初始化之前还是初始化之后,每次调用getinstance时都会进行加锁,也就意味着即使初始化之后是线程安全的,但还是存在着大量不必要的锁竞争!!
改进方案:
调用getinstance,instance 初始化之前才进行加锁,而初始化之后就不再加锁了。所以只需要在外层再加一次条件判断即可,判断当前instance是否已经初始化完成,如下:
public static Singleton2 getInstance() {
//判断是否初始化,若没有初始化就存在线程不安全,得加锁
if (instance == null) {
synchronized (Singleton2.class) {
if (instance == null) {//判断是否要创建实例
instance = new Singleton2();
}
}
}
return instance;
}
解决大量锁竞争的问题后,此时还存在一个很大的缺陷,如下:
执行 instance = new instance() 时,需要经历三个步骤,1.创建内存 2.针对内存进行初始化 3.把内存地址赋值给引用.而在这里是有可能触发指令重排序的,乱序执行. 若执行顺序变为了1-3-2,此时其他线程就可能看到一个非null的引用,但实际访问的时候却会出现一个非法的现象.
解决办法: 给 instance 加上 volatile关键字,如下:
private static volatile Singleton2 instance = null;
完整代码:
注意: 两个if中的条件并不一样,这里只是巧合
以上就是线程安全的懒汉模式,最核心的三个地方,1.在合适的地方加锁 2. 进行双重if判定
3.加上volatile关键字