你见过这样的单例模式吗?

640?wx_fmt=jpeg


今天这篇来自 SilenceDut 的投稿,并非讲解单例的几种写法,而是从synchronized与volatile这两个关键字说起,进而分析了一种不常见的单例写法。


SilenceDut 的博客地址:

http://www.jianshu.com/users/6b2e4b40b8ce


synchronized关键字


synchronized 关键字是用来控制线程同步的,就是在多线程的环境下,控制 synchronized 代码段不被多个线程同时执行。是一种阻塞性的锁,synchronized 既可以加在一段代码上,也可以加在方法上。


synchronized(this) 非static的synchronized方法,只能防止多个线程同时执行 同一个对象的同步代码段。当 synchronized 锁住一个对象后,别的线程如果也想拿到这个对象的锁,就必须等待这个线程执行完成释放锁,才能再次给对象加锁,这样才达到线程同步的目的。即使两个不同的代码段,都要锁同一个对象,那么这两个代码段也不能在多线程环境下同时运行。


所以我们在用 synchronized 关键字的时候,能缩小代码段的范围就尽量缩小,能在代码段上加同步就不要再整个方法上加同步。这叫 减小锁的粒度,使代码更大程度的并发。原因是基于以上的思想,锁的代码段太长了,别的线程是不是要等很久。


如果用 synchronized 加在 静态方法 上,就相当于用 ××××.class 锁住整个方法内的代码块,此时是锁住该类的Class对象,相当于一个全局锁。使用 synchronized 修饰的方法或者代码块可以看成是一个原子操作。


一个线程执行互斥代码过程如下:


  1. 获得同步锁;

  2. 清空工作内存;

  3. 从主内存拷贝对象副本到工作内存;

  4. 执行代码(计算或者输出等);

  5. 刷新主内存数据;

  6. 释放同步锁。


所以,synchronized 既保证了多线程的并发有序性,又保证了多线程的内存可见性。


volatile(非阻塞性的)


volatile 的特性当我们声明共享变量为 volatile 后,对这个变量的读/写将会很特别。理解 volatile 特性的一个好方法是:把对 volatile变量 的单个读/写,看成是使用同一个监视器锁对这些单个读/写操作做了同步。


它的工作原理是,它对写和读都是直接操作工作主存的。下面我们通过具体的示例来说明,请看下面的示例代码:


640?wx_fmt=png


假设有多个线程分别调用上面程序的三个方法,这个程序在语意上和下面程序等价:


640?wx_fmt=png


临界区代码的执行具有原子性。这意味着即使是 64位的long型 double型 变量,只要它是 volatile 变量,对该变量的读写就将具有原子性。


如果是 多个volatile 操作或类似于 volatile++ 这种复合操作,这些操作整体上不具有原子性。简而言之,volatile变量 自身具有下列特性:


  • 可见性: 对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。

  • 对 volatile 变量的写操作,不允许和它之前的读写操作打乱顺序;对 volatile 变量的读操作,不允许和它之后的读写乱序


volatile不能保证原子性,原子性不是 volatile 来保证的,如果操作本来就具有原子性,volatile 就会保证这个原子性不会被打破,理解为加上同步。比如上面例子中的 get set 函数。


下面这段 singleton(单例模式)的实现是线程不安全的,尽管使用了 volatile


640?wx_fmt=png


关于原子性的理解:只对 单个操作 就有原子性,比如i++这种就不是单个操作。


但是,轻易不要用volatile来替代synchronized来避免并发问题,因为很多原子操作自己可能并没那么了解,除非你是并发专家。《java编程思想》的作者是这样建议的。


单例模式的应用


“饿汉”模式


640?wx_fmt=png


这种方式比较常用,但容易产生垃圾对象。


优点:没有加锁,执行效率会提高。


缺点:类加载时就初始化,浪费内存。


它基于 Classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法,比如反射)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。


双重锁定模式


640?wx_fmt=png


讨论下 volatile 关键字的必要性,如果没有 volatile 关键字,问题可能会出在singleton = new Singleton() 这句,用伪代码表示:


// 分配内存
inst = allocat();
// 赋值
sSingleton = inst;
// 真正执行构造函数
constructor(inst);


可能会由于虚拟机的优化等导致赋值操作先执行,而构造函数还没完成,导致其他线程访问得到 singleton 变量不为null,但初始化还未完成,导致程序崩溃。参考自:


Java单例真的写对了么?

http://www.race604.com/java-double-checked-singleton


一种新的单例模式


最近在读 RxJava 的源码时,见到了一种新的单例模式,可能是自己见识太少,之前对这种方式真的没见过,也可以说闻所未闻。由此引发了一些对 AtomicReference 相关原子类的思考的研究,先看代码:


640?wx_fmt=png


先看下 AtomicReference 的源码:


640?wx_fmt=png


AtomicReference 是作用是对"对象"进行原子操作。通过源码可以看出,它是通过 "volatile"和"Unsafe 提供的CAS(比较与交换,Compare and swap,是一种有名的无锁算法函数)实现原子操作。


CAS

https://www.ibm.com/developerworks/cn/java/j-jtp04186


  • current volatile 类型。这保证了:当某线程修改value的值时,其他线程看到的value值都是最新的,即修改之后的volatile的值。


  • 通过 CAS 设置 value。这保证了:当某线程池通过CAS函数(如compareAndSet函数)设置 value 时,它的操作是原子的,即线程在操作value时不会被中断。CAS是一种无阻塞的锁,采用不断比较设值的方式来避免并发问题,不会有锁的等待和上下文切换问题,性能消耗较小。


如果 当前值 == 预期值,则以原子方式将该值设置为给定的更新值。


这种方式既能够保证延迟加载又能保证原子性及实例的唯一性,代码也相对比较简洁。


通过并发的学习与使用,线程的阻塞和上下文的切换会带来一定的性能开销,尤其在高并发的环境下。


而原子变量可以避免优先级倒置和死锁等危险,竞争比较便宜,协调发生在更细的粒度级别,允许更高程度的并行机制等等。




640?wx_fmt=png

如果你有好的技术文章想和大家分享,欢迎向我的公众号投稿,投稿具体细节请在公众号主页点击“投稿”菜单查看。


欢迎长按下图 -> 识别图中二维码或者扫一扫关注我的公众号:

640?wx_fmt=jpeg