前言
什么是单例模式?
其实用通俗的话就是程序猿约定俗成的一些东西,就比如如果你继承了一个抽象类,你就要重写里面的抽象方法,如果你实现了一个接口,你就要重写里面的方法。如果不进行重写,那么编译器就会报错。这其实就是一个规范。
而单例模式能保证某个类在程序中只存在唯一的一个实例,而不会创建出多个实例
那么,单例模式又分成“饿汉”和“懒汉”两种、
一.饿汉模式
顾名思义,饿汉模式就是在类加载的时候,创建实例。
package thread; //期待这个类能有唯一实例 public class hungryDemo { private static hungryDemo instance = new hungryDemo(); public static hungryDemo getInstance() { return instance; } //把构造方法设置为私有,这样在类外就无法 new 出这个对象的实例了 private hungryDemo() { } }
代码解读:
1. 首先创建了一个 hungryDemo 类,里面有一个类方法和一个类变量
2. 我们将构造方法设置为了private,那么在类外就无法再针对 hungryDemo 再实例化类了
我们现在在类外,通过 hungryDemo提供的 public static hungryDemo getInstance 方法来进行调用,可以发现如下结果:
class Demo1 { public static void main(String[] args) { hungryDemo h1 = hungryDemo.getInstance(); hungryDemo h2 = hungryDemo.getInstance(); System.out.println(h1 == h2); } }
运行结果:
可以发现,两者获取到的类对象引用是一致的,那么单例模式的饿汉版本就创建好了。
二.懒汉模式
🎈单线程版本
我们的预期结果是不变的,那就是要实现单例模式,也就是这个类 只能被实例化一次!!!
那么懒汉模式顾名思义,也就是类加载的时候不创建实例,第一次使用的时候才创建实例。
那么我们可以写出以下代码:
package thread; public class lazyDemo { private static lazyDemo instance = null; public static lazyDemo getInstance() { /** * 只有调用该方法的时候,才创建对象 */ if (instance == null) { instance = new lazyDemo(); } return instance; } private lazyDemo() { } }
代码解读:
🍺首先,设置类成员变量 instance 为 null,当第一次使用getInstance()的时候才进行创建对 象。
🍺其次,跟饿汉模式一样,将类的构造方法设置为 private ,类外无法再次创建对象。
🍺最后,在getInstance方法中判断 instance 是否为空,为空那就创建对象。为空说明已经 被调用一次了,那么就直接返回 instance 引用。
🎈🎈多线程版本 1
在以上的单线程版本中,我们不难发现以下问题:
假设现在有两个线程,他们是按照如下的顺序来执行的:
那么此时的代码就会出现问题: t1 线程首先判断了 instance 是否为空,此时 t2 线程来运行了,也判断 instance 是否为空,紧接着 instance不为空,然后就创建了对象! 然后再回到 t1 线程中,又要进行创建对象。 此时问题已经很明显了,那就是 由于if代码块在多线程中的执行顺序问题导致的
更精简一下:
就是 instance = new lazyDemo() 是写操作, instance == null 是读操作,在多线程中,如果一段代码即涉及读操作,又设计写操作,那么就很容易出现问题!!!
🍭解决办法:
当一段代码是因为读写操作出BUG,我们首先想到的就是加锁。也就是在我写的时候,你不要 读。我读的时候,你不要写。
synchronized
是一种内置的 Java 关键字,它用于实现线程的同步。当一个线程进入synchronized
块或方法时,它获得了锁,这会阻止其他线程同时进入相同的synchronized
块或方法,从而确保了共享资源的互斥访问。
修改代码如下:
package thread; public class lazyDemo { private static lazyDemo instance = null; public static lazyDemo getInstance() { /** * 只有调用该方法的时候,才创建对象 */ synchronized (lazyDemo.class) { //1. 加锁解决的是线程安全问题(确保是单例模式,只new一次) if (instance == null) { instance = new lazyDemo(); } } return instance; } private lazyDemo() { } }
对于对象lazyDemo.class,实际上就是lazyDemo这个类,也就是对类进行加锁。
此时加锁之后,当t1线程进行读写操作的时候,t2线程再次进行访问就只能进行阻塞。
此时t1就可以放心创建出一个对象出来,此时t2再进行调用方法的时候,instance 不为空,就直接返回 t1 创建好的对象引用。 这时候就确保了只创建出一个实例。
🎈🎈🎈 多线程版本2
其实,多线程版本1 还是有问题的,我们发现:如果t1 线程加锁后创建好了对象,其他线程(t2,t3,t4.........)在进行访问的时候,首先就要进行加锁操作。 也就是每次访问都要进行加锁,这是一个资源开销非常大的操作。
深入探究一下,我们发现其他线程(t2,t3,t4.........)在进行访问的时候,只需要判断当前的对象是否被创建好了即可。如果被创建好了,那么就直接返回对象引用。如果没有被创建好,再进行加锁创建对象。
修改代码如下:
public class lazyDemo { private static lazyDemo instance = null; public static lazyDemo getInstance() { /** * 只有调用该方法的时候,才创建对象 */ if(instance == null) { //2. if判断解决的是多次加锁,加锁频率太高的问题 synchronized (lazyDemo.class) { //1. 加锁解决的是线程安全问题(确保是单例模式,只new一次) if (instance == null) { instance = new lazyDemo(); } } } return instance; } private lazyDemo() { } }
在多线程中,这两个 if 的作用大不相同!!!
修改后,我们发现如果 t1 线程创建好了对象, 此时其他线程(t2,t3,t4.........)在进行调用的时候,首先判断了instance 是否为空,不为空就说明已经创建好了对象~
🎈🎈🎈🎈多线程版本3
其实到现在,这个懒汉模式的单例代码还是有问题!!!
在多线程下,要考虑到编译器的优化问题,当编译器没有按照我们的逻辑进行操作的时候,那么就会出现问题。
在此代码中,new 操作可以分为以下三步:
1.申请内存空间(一定先执行),获取到内存地址
2.在内存空间上构造对象(构造方法)
3.把内存的地址,赋值给 instance 引用
在单线程环境下,执行那种顺序都无所谓,但是如果在多线程环境下,就可能出现问题:
假设是按照 1 3 2 的顺序来执行,当 1 和 3 操作执行完的时候,instance 已经非空了,只是内存空间上还没有构造对象 / 方法,此时instance 指向的是一个还没初始化的非法对象。 此时此刻 t2 进行访问,判断 instanc 是不为空的,然后就返回了一个还没初始化的非法对象,进一步 t2 线程就有可能访问 instance 里面的属性和方法。此时就出现了问题了。
这个问题就是指令重排序问题,解决办法就是让 instance 加入上volatile 关键字,此时就避免了指令重排序问题。
//3.加 volatile是为了解决new 操作的指令重排序问题
private volatile static lazyDemo instance = null;
此时的代码就会严格按照 1 2 3 的顺序执行。
总结:单例模式是一个约定俗成的规范,保证一个类只能实例化一个对象。饿汉模式在多线程和单线程都没有问题,因为一开始它就创建好了对象。 而懒汉模式的多线程版本会出现以下三个问题:1. 线程安全问题( 确保只new 一次)2. 多次重复加锁的问题 3. 指令重排序问题。
希望以上的解决办法对你有所帮助!!!