本文章是在学习了 微信公众号 “java后端技术 ” 以后本身的学习笔记 。 其中直接 复制了 至关部分的原做者的原文。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/