【多线程】单例模式
文章目录
- 1. 单例模式
- 1.1 什么是单例模式
- 1.2 为什么使用单例模式
- 1.3 实现单例模式
- 1.3.1 饿汉模式
- 1.3.1 懒汉模式
1. 单例模式
1.1 什么是单例模式
单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供一个全局访问点来访问该实例。
单例 =单个实例(对象)
1.2 为什么使用单例模式
使用单例模式,就可以对咱们的代码进行一个更严格的校验和检查。
示例:
有的时候代码中,需要使用一个对象,来管理/持有大量的数据,此时有一个对象就可以了。比如,一个对象管理了10G的数据,如果你不小心创建出多个对象,内存空间就会成倍增长,机器就顶不住了。
期望:
让机器 (编译器)能够对代码中的指定的类,创建的实例个数进行校验,如果发现创建多个实例了,就直接编译报错这种。如果能做到这一点,就可以非常放心的编写代码,不必担心因为失误创建出多个实例了。
1.3 实现单例模式
1.3.1 饿汉模式
饿汉模式是单例模式的一种实现方式,其核心特点是在类加载时就创建单例对象,确保在程序运行期间该类的单例对象始终存在。
饿汉模式在多线程中天然就是线程安全的
class Singleton {
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
private Singleton() {}
}
public class Danli1 {
public static void main(String[] args) {
// Singleton s = new Singleton();
Singleton s1 = Singleton.getInstance();
Singleton s2 = Singleton.getInstance();
System.out.println(s1 == s2);
}
}
这个引用就是我们希望创建出唯一实例的引用。static 静态的,指的是类属性, instance就是Singleton类对象里面持有的属性。每个类的类对象只有一个,类对象的static属性自然也就只有这一个。
private static Singleton instance = new Singleton();
这段代码的作用就是想要使用这个类的实例,就需要通过这个方法来获取实例。不应该在其他的代码中重新new这个对象,而是直接调用这个方法来获取到现成的对象。
public static Singleton getInstance() {
return instance;
}
单例模式的点睛之笔,为了防止其他类new一个对象,这里直接将构造方法私有化,其他类调用不了构造方法自然也就创建不了这个实例。
1.3.1 懒汉模式
懒汉模式是单例模式的一种实现方式,其核心特点是在第一次需要使用单例对象时才进行实例化,而不是像饿汉模式那样在类加载时就创建实例,这样可以节省资源。
package Thread;
/**
* 懒汉模式
* */
class SingletonLazy {
private static Object locker = new Object();
private volatile static SingletonLazy instance = null;//注意这个volatile关键字的使用
public static SingletonLazy getInstance() {
//如果instance为null,就说明是首次调用,首次调用需要考虑到线程安全的问题,就要加锁
//如果是非null,就说明是后续的操作,就不必加锁
if (instance == null) {
synchronized (locker){
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy() {}
}
public class Danli2 {
public static void main(String[] args) {
SingletonLazy s1 = SingletonLazy.getInstance();
SingletonLazy s2 = SingletonLazy.getInstance();
System.out.println(s1 == s2);
}
}
关键问题:在多线程中,并发调用getInstance方法,这两个代码是否线程安全?
对于饿汉来说,getlnstance 直接返回 Instance 实例。这个操作本质上就是"读操作",线程安全。
对于懒汉来说,getlnstance 先判断再创建 Instance 实例。这个操作有读也有写,线程不安全(上述代码通过修改改为线程安全)。
修改方案:
1)加锁
在多线程中,有可能线程1执行到判断,切换到了线程2执行完了所有创建实例,这时切换回线程1时,线程1在已经判断的基础上又创建了新的实例,bug出现!!!
为了让代码执行正确,将 if 和 new 两个操作,打包成一个原子
synchronized (locker){
if (instance == null) {
instance = new SingletonLazy();
}
}
2)双重校验锁
在多线程中,如果instance已经被创建,应该快速的返回该实例。但是判断操作还是在锁里,每次调用都先加锁再解锁,此时效率就非常低了!!!(因为加锁就意味着可能会产生阻塞)
为了提高效率,在锁的外层加一个判断,判定是否需要加锁。如果是第一次调用该方法,也就是创建实例对象,则进行加锁解锁;如果是第二次之后调用该方法,则仅需返回第一次创建好的实例对象就行,提高效率。
public static SingletonLazy getInstance() {
//如果instance为null,就说明是首次调用,首次调用需要考虑到线程安全的问题,就要加锁
//如果是非null,就说明是后续的操作,就不必加锁
if (instance == null) {
synchronized (locker){
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
3)指令重排序
指令重排序就JVM编译器优化的一种方式,调整原有代码的执行顺序,保证逻辑不变的情况下,提高程序的效率,但是在提升效率的同时会引起线程安全问题。
instance = new SingletonLazy();
对于上面这一个行代码,可以拆分为三大步骤:
(1)申请一段内存空间
(2)在这个内存上调用构造方法,创建出这个实例
(3)把这个内存地址赋值给 instance 引用变量
正常情况下,上述代码是按照1.2.3的顺序执行的,但是编译器也会优化成1.3.2的顺序执行,此时,指令重排序就会造成问题。
如果线程1先执行1.3,调度走,此时instance不是null,但是指向的其实是一个尚未初始化的对象。但是未初始化的对象会被线程2判定为 instance != null ,就会直接 return。如果线程2继续使用 instance 里面的属性和方法,就会出现问题(此时这里的属性都是未初始化的“全0”值)。就可能会引起代码的逻辑出现问题。
为了解决这问题,还是引入 volatile 。
volatile 有两个功能
1.保证内存可见性,每次访问变量必须都要重新读取内存,而不会优化到寄存器/缓存中
2.禁止指令重排序。针对这个被 volatile 修饰的变量的读写操作相关指令,是不能被重排序的!!!
private volatile static SingletonLazy instance = null;//注意这个volatile关键字的使用