设计模式 之 单列设计模式
本文章是在学习了 微信公众号 “java后端技术 ” 之后自己的学习笔记 。 其中直接 复制了 相当部分的原作者的原文。
如果您看到了我的这篇文章, 推荐您 查看原文
原文连接 : https://mp.weixin.qq.com/s/CfekzTTT-a066_PyT_n_eA
在以前自己也了解过一些设计模式, 这其中就包括了单例模式, 但是 对单例模式只限于 基本的 懒汉式 和 饿汉式 :
饿汉式代码示例 :
public DemoSingle{ //私有化构造器 private DemoSingle(){} //提前构造好方法 private static DemoSingle single = new DemoSingle(); //提供暴露对象的方法 public DemoSingle getDemoSingle(){ return single; } }
懒汉式代码示例 :
/** * Create by yaoming on 2018/4/27 */ public class DemoSingle { //私有化构造方法 private DemoSingle(){} //私有化 本类对象引用 private static DemoSingle single = null; //得到本类方法的引用 public DemoSingle getDemoSingel(){ synchronized (DemoSingle.class){ if(single == null){ single = new DemoSingle(); } } return single; } }
所谓单列模式就是说, 全局在任何一个地方发使用到的该类对象都是同一个对象,首先要保证 对象一直存在(一直有引用指向对象),所以,使用一个静态引用
指向该类。 同时要保证 只有一个对象, 所以要私有化 构造方法, 使得只有自己能构造这个对象(而且自己必须构造且之构造一个该对象)。
饿汉式 是在加载该类的时候就进行了对象的建立,无论我们是否使用到了 这个对象。 其安全有效, 不涉及多线程操作。 但是其造成了资源的浪费。
懒汉式 在实际情况中我们可能为了性能着想, 往往希望能使用延迟加载的方式来创建对象, 这个就是懒汉式了。
上面的懒汉式代码,为了考虑多线程的关系, 加了一个同步代码块, 这样虽然解决了 多线程安全问题, 但是却因为每次都会进行一个同步情况下的判断,
往往使得效率并,并没有增加, 用原文作者的话来说就是 : 使用一个 百分之百的盾 来 阻挡一个 百分之一 的出现的问题。 这显然不合适。
遂优化 :
public class DemoSingle { //私有化构造方法 private DemoSingle(){} //私有化 本类对象引用 private static DemoSingle single = null; //得到本类方法的引用 public DemoSingle getDemoSingel(){ if(single == null){ synchronized (DemoSingle.class){ if(single == null){ single = new DemoSingle(); } } } return single; } }
这个代码就是原来我对于懒汉式的理解了, 在看了原作者的文章后, 才发现在这个看似完美的代码下面隐藏的问题,
这里 原作者 谈到了两个概念 : 原子操作 和 指令重排
这里是作者原文 :
原子操作:
简单来说,原子操作(atomic)就是不可分割的操作,在计算机中,就是指不会因为线程调度被打断的操作。比如,简单的赋值是一个原子操作:
m = 6; // 这是个原子操作 |
假如m原先的值为0,那么对于这个操作,要么执行成功m变成了6,要么是没执行 m还是0,而不会出现诸如m=3这种中间态——即使是在并发的线程中。
但是,声明并赋值就不是一个原子操作:
int n=6;//这不是一个原子操作 |
对于这个语句,至少有两个操作:①声明一个变量n ②给n赋值为6——这样就会有一个中间状态:变量n已经被声明了但是还没有被赋值的状态。这样,在多线程中,由于线程执行顺序的不确定性,如果两个线程都使用m,就可能会导致不稳定的结果出现。
指令重排:
简单来说,就是计算机为了提高执行效率,会做的一些优化,在不影响最终结果的情况下,可能会对一些语句的执行顺序进行调整。比如,这一段代码:
int a ; // 语句1 |
正常来说,对于顺序结构,执行的顺序是自上到下,也即1234。但是,由于指令重排
的原因,因为不影响最终的结果,所以,实际执行的顺序可能会变成3124或者1324。
由于语句3和4没有原子性的问题,语句3和语句4也可能会拆分成原子操作,再重排。——也就是说,对于非原子性的操作,在不影响最终结果的情况下,其拆分成的原子操作可能会被重新排列执行顺序。
OK,了解了原子操作和指令重排的概念之后,我们再继续看代码三的问题。
主要在于singleton = new Singleton()这句,这并非是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情。
1. 给 singleton 分配内存
2. 调用 Singleton 的构造函数来初始化成员变量,形成实例
3. 将singleton对象指向分配的内存空间(执行完这步 singleton才是非 null了)
在JVM的即时编译器中存在指令重排序的优化。
也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
再稍微解释一下,就是说,由于有一个『instance已经不为null但是仍没有完成初始化』的中间状态,而这个时候,如果有其他线程刚好运行到第一层if (instance ==null)这里,这里读取到的instance已经不为null了,所以就直接把这个中间状态的instance拿去用了,就会产生问题。这里的关键在于线程T1对instance的写操作没有完成,线程T2就执行了读操作。
由此可见, 我的第二段 懒汉式代码存在 隐患 , 根据作者思路 将之改为 :
public class DemoSingle { //私有化构造方法 private DemoSingle(){} //私有化 本类对象引用 private static volatile DemoSingle single = null; //得到本类方法的引用 public DemoSingle getDemoSingel(){ if(single == null){ synchronized (DemoSingle.class){ if(single == null){ single = new DemoSingle(); } } } return single; } }
其实就是加上了一个 volatitle 关键字 , 这里 volatitle 关键字的作用是禁止 指令重排, 在对 single 进行复制完成之前是不会进行 读操作的。
(作者原文 : 注意:volatile阻止的不是singleton = new Singleton()这句话内部[1-2-3]的指令重排,而是保证了在一个写操作([1-2-3])完成之前,不会调用读操作(if (instance == null))。)
这样就解决了传统的 懒汉式单例模式 的多线程安全问题, 除此之外 原作者还提供了 其他两种更为简便的 方式:
静态内部类:
public class DemoSingle { //私有化构造方法 private DemoSingle(){} //静态内部类 private static class DemoSingleHand{ private static final DemoSingle DEMO_SINGLE = new DemoSingle(); } //获得该类对象的方法 public static DemoSingle getDemoSingel(){ return DemoSingleHand.DEMO_SINGLE; } }
这种写法的巧妙之处在于:对于内部类SingletonHolder,它是一个饿汉式的单例实现,在SingletonHolder初始化的时候会由ClassLoader来保证同步,使INSTANCE是一个真单例。
同时,由于SingletonHolder是一个内部类,只在外部类的Singleton的getInstance()中被使用,所以它被加载的时机也就是在getInstance()方法第一次被调用的时候。
它利用了ClassLoader来保证了同步,同时又能让开发者控制类加载的时机。从内部看是一个饿汉式的单例,但是从外部看来,又的确是懒汉式的实现
枚举:
是不是很简单?而且因为自动序列化机制,保证了线程的绝对安全。三个词概括该方式:简单、高效、安全
这种写法在功能上与共有域方法相近,但是它更简洁,无偿地提供了序列化机制,绝对防止对此实例化,即使是在面对复杂的序列化或者反射攻击的时候。虽然这中方法还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。
原文地址:https://gyl-coder.top/Java%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F--%E5%8D%95%E4%BE%8B%E6%A8%A1%E5%BC%8F/