✨✨hello,愿意点进来的小伙伴们,你们好呐!
系列专栏:【JavaEE初阶】
本篇内容:基于多线程的单例模式
作者简介:一名现大二的三非编程小白,日复一日,仍需努力。
单例模式是设计模式中很常见的一种,属于大佬们为了像我这种小菜鸟能够将代码写得水平好像还可以 , 针对一些经典的场景 , 发明出来的一种"棋谱",给出的一些典型的解决方案中的一种
单例模式分为 饿汉式 和 懒汉式 两种
在某些场景下,就需要对一个类只能创建一个实例,那么像这种需求,单例模式就应运而生了,使用了单例模式后,想创建多个实例都要费尽心思也没办法创建,
public class Singleton1 {
private static Singleton1 instance = new Singleton1();
public static Singleton1 getInstance() {
return instance;
}
private Singleton1() {}
}
上述代码就是单例模式中的饿汉模式:
- 在类中先将类的实例对象创建出来,且赋给了静态的属性,让这个属性在类加载的时候就创建存在了,且这个类只有一份类属性,所以保证了这个实例对象只有一份.
- 如果想要获取到该对象,就使用get方法,该方法直接返回以及创建好的静态对象.统一提供Singleton1.getInstance() 来获取该实例对象.
- 为了避免 Singleton1 类不小心被复制出多份,把构造方法给私有化,所以在类的外部就无法创建该类的对象了
饿汉模式使用start保证了这个实例的唯一,然后再类加载的时候就创建该类的实例对象,将构造方法私有化,保证在类的外部无法new这个对象;
该模式被称为饿汉式的原因可以理解为:该模式在类加载的时候就创建了对象,显得很急切,很饿的感觉,所以将其称为饿汉模式.
public class Singleton2 {
private static Singleton2 instance = null;
public static Singleton2 getInstance() {
if(instance == null) {
instance = new Singleton2();
}
return instance;
}
private Singleton2() {}
}
上述代码就是单例模式中的懒汉式:
- 懒汉式对比饿汉式的区别就是创建对象的时机,饿汉式创建对象是在加载类的时候创建的,而懒汉式在类加载的时候是定义了类的属性,然后这个属性指向null;
- 然后在get方法中,判断是否这个属性为null,如果是第一次要使用该类的实例,对象为null就创建对象,不为null就直接返回之前创建好的类实例属性.
- 构造方法也是私有化处理
✨✨✨
懒汉式的创建对象是在要用再来创建,这样子的模式似乎有点懒,但是对于程序来说,懒是褒义词,懒说明资源不会浪费很严重,这样子的创建模式可以避免在用不上对象实例的时候,并不会创建对象实例,避免了资源的浪费
上面讲的两个单例模式是否能经得起多线程的敲打呢?在日常环境下,大多数都是多线程场景;
在饿汉模式下,类的实例对象是在类加载阶段就创建的且Java中规定只能创建一个类属性,然后饿汉式代码中就只是对该类属性进行从主内存中读取到线程的本地内存中,然后线程访问到进行返回类实例对象.
这种情况下操作是原子操作,无法继续分解了,然后该实例对象是一开始就存在的,所以也并不存在内存可见性的情况;
所以在饿汉式中,代码是线程安全的
接下来来看看懒汉式:
public class Singleton2 {
private static Singleton2 instance = null;
public static Singleton2 getInstance() {
if(instance == null) {
instance = new Singleton2();
}
return instance;
}
private Singleton2() {}
}
在懒汉式的代码中,最开始类加载阶段创建的是一个类属性,然后这个属性指向是null,然后在get方法中,该类是先将类属性从主内存中读取(load),然后再进行判断(cmp),然后依照判断后的操作进行new <<先申请一个内存可见,调用构造方法将这个内存初始化为一个合理的对象,把内存地址空间赋给类属性的引用 >>,然后写入内存(save),然后返回实例对象;或者直接返回
上面的操作虽然每一步都是原子操作,但是其实都是非原子操作分解出来的,所以懒汉式其实是会引起线程安全问题的.
下面我们来看看怎么避免懒汉式中的线程安全问题>>
public class Singleton2 {
private static Singleton2 instance = null;
public static Singleton2 getInstance() {
if(instance == null) {
instance = new Singleton2();
}
return instance;
}
private Singleton2() {}
}
这个是单线程下的懒汉式代码,我们来分析一下会有什么原因引发的线程安全
因为操作的非原子性,所以CPU的调度会将其分为原子操作去执行,最后会导致我们不想要的执行结果,最后导致了线程安全问题.
所以下面,我们先来解决原子操作的问题;
public class Singleton2 {
private static Singleton2 instance = null;
public static Singleton2 getInstance() {
synchronized (Singleton2.class) {
if(instance == null) {
instance = new Singleton2();
}
}
return instance;
}
private Singleton2() {}
}
加锁后会将该操作视为原子操作:
但是这样子的加锁方式其实会导致资源的浪费,因为这样子的加锁会导致每一次调用get方法的时候都会加锁,然后释放锁,其实是没有必要的,加锁操作在第一次要创建该对象实例的时候加锁就行了.
public class Singleton2 {
private static Singleton2 instance = null;
public static Singleton2 getInstance() {
if(instance == null) {
synchronized (Singleton2.class) {
if(instance == null) {
instance = new Singleton2();
}
}
}
return instance;
}
private Singleton2() {}
}
优化到了什么的代码,上面的代码中get方法里,第一个if判断是为了判断是否是第一次创建对象,需不需要加锁,第二个if判断是判断需不需要创建对象;
这样子的两个判断可以有效地避免对加锁操作的频繁使用,只对第一次创建对象加锁即可.
但是这样子的代码其实也是有问题的 ! 内存可见性问题 ! ! ! 指令重排序问题 ! ! !
要是很多线程都去进行 getinstance() ,这个时候,有可能会有JVM优化的风险,最后可能导致只有第一次读取才是真正的内存的值,后面的读取都是读线程本地内存的值.导致内存可见性问题
也有可能因为编译器的优化指令重排序,导致了new这个执行步骤的优化,最后也会产生线程安全问题.
所以我们在属性中加了 volatile 来避免出现内存可见性和指令重排序的两个问题;
public class Singleton2 {
private volatile static Singleton2 instance = null;
public static Singleton2 getInstance() {
if(instance == null) {
synchronized (Singleton2.class) {
if(instance == null) {
instance = new Singleton2();
}
}
}
return instance;
}
private Singleton2() {}
}
这个时候我们的代码才优化完成,适用于多线程环境下的懒汉式的单例模式