【Java笔记】单例模式
目录
- 1. 饿汉模式
- 2. 懒汉模式
- 2.1 线程不安全
- 2.2 线程不安全原因分析
- 2.2.1 原因
- 2.2.2 给 new SingletonLazy() 加锁
- 2.2.3 在外层加锁
- 2.2.4 存在的问题
- 3. 双重检查锁(Double Check Lock,DCL)(重要)
- 3.1 DCL解释
- 3.2 重要补充:volatile关键字的作用
单例模式是保证一个类在整个应用程序中只有一个实例,同时提供一个统一的全局访问入口,避免因频繁创建对象造成内存浪费或状态不一致的问题。
1. 饿汉模式
代码:
public class SingletonHungry {// 定义成员变量,使用static修饰保证全局唯一private static SingletonHungry instance = new SingletonHungry();// 构造方法私有化, 禁止外部实例化对象private SingletonHungry() {}// 加 static 将方法编程静态代码块,属于类,通过 类名.方法名 的方式调用public static SingletonHungry getInstance() {return instance;}
}
把这种类加载的时候就完成对象初始化的创建方式称为 “饿汉模式”。
调用代码:
public static void main(String[] args) {SingletonHungry instance1 = SingletonHungry.getInstance();System.out.println(instance1);SingletonHungry instance2 = SingletonHungry.getInstance();System.out.println(instance2);SingletonHungry instance3 = SingletonHungry.getInstance();System.out.println(instance3);}
获取到的都是同一个对象
2. 懒汉模式
2.1 线程不安全
代码:
public class SingletonLazy {// 不初始化private static SingletonLazy instance;private SingletonLazy() {}// 对外提供一个获取对象的方法public static SingletonLazy getInstance() {if (instance == null) {instance = new SingletonLazy();}return instance;}
}
这种方法在单线程中得到的对象都是同一个,但是在多线程环境下会有线程安全问题!
创建10个线程,调用 SingletonLazy :
public static void main(String[] args) {for (int i = 0; i < 10; i++) {Thread thread = new Thread(() -> {SingletonLazy instance = SingletonLazy.getInstance();System.out.println(instance);});thread.start();}
}
执行结果:
得到的对象不是同一个,存在线程安全问题。
2.2 线程不安全原因分析
2.2.1 原因
有多少个线程判断了 instance == null,就会 new 多少个对象!
2.2.2 给 new SingletonLazy() 加锁
public static SingletonLazy getInstance() {// 第一次判断是否需要加锁if (instance == null) {synchronized (SingletonLazy.class) {instance = new SingletonLazy();}}return instance;
}
此时的代码依旧是线程不安全的!!!
原因:
有多少个线程进入了 if 代码块,就会有多少个对象被实例化!
2.2.3 在外层加锁
代码:
public static SingletonLazy getInstance() {// 第一次判断是否需要加锁synchronized (SingletonLazy.class) {if (instance == null) {instance = new SingletonLazy();}}return instance;}
不会出现线程安全问题了。
有两个线程 t1 和 t2 ,t1 先拿到锁资源,只有线程 t1 全部执行完释放锁之后 t2 才有可能拿到锁资源,但此时 对象已经不为 null 了,就不会进入 if 代码块,就不会再次创建对象。
2.2.4 存在的问题
- 当第一个线程进入这个方法时,如果变量没有初始化,则获取锁进行初始化操作,此时单例对象被第一个线程创建完成;
- 后面的线程以后也永远不会再执行new对象的操作;
- synchronized还有没有必要加了?
当第一个线程把对象创建好之后,就没有必要了,从第二个线程开始这个加锁解锁都是无效的操作,synchronized 关键字对应了CPU中的指令,LOCK 和 UNLOCK 对应的锁指令是互斥锁,比较消耗系统资源。
解决办法:在加锁前再次判断一下是否需要加锁
3. 双重检查锁(Double Check Lock,DCL)(重要)
3.1 DCL解释
代码:
public class SingletonDCL {private static volatile SingletonDCL instance;private SingletonDCL() {}// 对外提供一个获取对象的方法public static SingletonDCL getInstance() {// 第一次判断是否需要加锁if (instance == null) {synchronized (SingletonDCL.class) {if (instance == null) {instance = new SingletonDCL();}}}return instance;}
}
解析:
- 有 t1 和 t2 两个线程,假设t1、t2 同时进入了 if 代码块并判断 instance == null, t1 先拿到了锁资源,再次判断instance 为 null,则创建了一个对象,此时 instance 不为空了,释放锁资源后返回了这个 instance;
- t2 拿到锁资源,此时 instance 不为空了,则不会进入第二个 if 代码块,直接释放锁资源返回已经创建好了的对象;
- 当有其他线程再次获取对象时,instance 不为空,则不会进入第一层 if 代码块,直接返回已经创建好了的对象,保证了单例。
3.2 重要补充:volatile关键字的作用
private static volatile SingletonDCL instance;
只要在多线程环境中修改了共享变量就要加 volatile ,主要是考虑到指令重排序的问题
new 一个对象的步骤:
- 在内存中申请一片空间
- 初始化对象的属性(赋初值)
- 把对象在内存中的首地址赋值给对象的引用
1 和 3 是强相关的,只有在分配完内存空间之后才会执行 3,但2并不是强相关的,可能会发生指令重排序
正常执行顺序:1、2、3
可能的重排序后顺序:1、3、2
重排序之后,在分配完内存空间后直接把对象在内存中的首地址赋值给对象的引用,此时的 instance 是一个尚未初始化完成的对象,其他线程如果访问这个未初始化完成的对象,就会导致出现错误!因此要加 volatile 禁止指令重排序!
同时也保证了可见性,确保一个线程修改了 instance 的值后,其他线程能立即看到最新值。