GitHub 6.6k Star 的Java工程师成神之路 ,不来了解一下吗?git
GitHub 6.6k Star 的Java工程师成神之路 ,真的不来了解一下吗?github
GitHub 6.6k Star 的Java工程师成神之路 ,真的肯定不来了解一下吗?编程
在个人博客和公众号中,发表过不少篇关于并发编程的文章,以前的文章中咱们介绍过了两个在Java并发编程中比较重要的两个关键字:synchronized和volatile多线程
咱们简单回顾一下相关内容:并发
一、Java语言为了解决并发编程中存在的原子性、可见性和有序性问题,提供了一系列和并发处理相关的关键字,好比synchronized、volatile、final、concurren包等。(再有人问你Java内存模型是什么,就把这篇文章发给他)性能
二、synchronized经过加锁的方式,使得其在须要原子性、可见性和有序性这三种特性的时候均可以做为其中一种解决方案,看起来是“万能”的。的确,大部分并发控制操做都能使用synchronized来完成。再有人问你synchronized是什么,就把这篇文章发给他。优化
三、volatile经过在volatile变量的操做先后插入内存屏障的方式,保证了变量在并发场景下的可见性和有序性。再有人问你volatile是什么,把这篇文章也发给他spa
四、volatile关键字是没法保证原子性的,而synchronized经过monitorenter和monitorexit两个指令,能够保证被synchronized修饰的代码在同一时间只能被一个线程访问,便可保证不会出现CPU时间片在多个线程间切换,便可保证原子性。Java的并发编程中的多线程问题究竟是怎么回事儿?线程
那么,咱们知道,synchronized和volatile两个关键字是Java并发编程中常常用到的两个关键字,并且,经过前面的回顾,咱们知道synchronized能够保证并发编程中不会出现原子性、可见性和有序性问题,而volatile只能保证可见性和有序性,那么,既生synchronized、何生volatile?指针
咱们都知道synchronized实际上是一种加锁机制,那么既然是锁,自然就具有如下几个缺点:
一、有性能损耗
虽然在JDK 1.6中对synchronized作了不少优化,如如适应性自旋、锁消除、锁粗化、轻量级锁和偏向锁等(深刻理解多线程(五)—— Java虚拟机的锁优化技术),可是他毕竟仍是一种锁。
以上这几种优化,都是尽可能想办法避免对Monitor(深刻理解多线程(四)—— Moniter的实现原理)进行加锁,可是,并非全部状况均可以优化的,何况就算是通过优化,优化的过程也是有必定的耗时的。
因此,不管是使用同步方法仍是同步代码块,在同步操做以前仍是要进行加锁,同步操做以后须要进行解锁,这个加锁、解锁的过程是要有性能损耗的。
关于两者的性能对比,因为虚拟机对锁实行的许多消除和优化,使得咱们很难量化这二者之间的性能差距,可是咱们能够肯定的一个基本原则是:volatile变量的读操做的性能小号普通变量几乎无差异,可是写操做因为须要插入内存屏障因此会慢一些,即使如此,volatile在大多数场景下也比锁的开销要低。
二、产生阻塞
咱们在深刻理解多线程(一)——Synchronized的实现原理中介绍过关于synchronize的实现原理,不管是同步方法仍是同步代码块,不管是ACC_SYNCHRONIZED仍是monitorenter、monitorexit都是基于Monitor实现的。
基于Monitor对象,当多个线程同时访问一段同步代码时,首先会进入Entry Set,当有一个线程获取到对象的锁以后,才能进行The Owner区域,其余线程还会继续在Entry Set等待。而且当某个线程调用了wait方法后,会释放锁并进入Wait Set等待。
因此,synchronize实现的锁本质上是一种阻塞锁,也就是说多个线程要排队访问同一个共享对象。
而volatile是Java虚拟机提供的一种轻量级同步机制,他是基于内存屏障实现的。说到底,他并非锁,因此他不会有synchronized带来的阻塞和性能损耗的问题。
除了前面咱们提到的volatile比synchronized性能好之外,volatile其实还有一个很好的附加功能,那就是禁止指令重排。
咱们先来举一个例子,看一下若是只使用synchronized而不使用volatile会发生什么问题,就拿咱们比较熟悉的单例模式来看。
咱们经过双重校验锁的方式实现一个单例,这里不使用volatile关键字:
1 public class Singleton {
2 private static Singleton singleton;
3 private Singleton (){}
4 public static Singleton getSingleton() {
5 if (singleton == null) {
6 synchronized (Singleton.class) {
7 if (singleton == null) {
8 singleton = new Singleton();
9 }
10 }
11 }
12 return singleton;
13 }
14 }
复制代码
以上代码,咱们经过使用synchronized对Singleton.class进行加锁,能够保证同一时间只有一个线程能够执行到同步代码块中的内容,也就是说singleton = new Singleton()这个操做只会执行一次,这就是实现了一个单例。
可是,当咱们在代码中使用上述单例对象的时候有可能发生空指针异常。这是一个比较诡异的状况。
咱们假设Thread1 和 Thread2两个线程同时请求Singleton.getSingleton方法的时候:
Step1 ,Thread1执行到第8行,开始进行对象的初始化。
Step2 ,Thread2执行到第5行,判断singleton == null。
Step3 ,Thread2通过判断发现singleton != null,因此执行第12行,返回singleton。
Step4 ,Thread2拿到singleton对象以后,开始执行后续的操做,好比调用singleton.call()。
以上过程,看上去并无什么问题,可是,其实,在Step4,Thread2在调用singleton.call()的时候,是有可能抛出空指针异常的。
之全部会有NPE抛出,是由于在Step3,Thread2拿到的singleton对象并非一个完整的对象。
咱们这里来分析一下,singleton = new Singleton();这行代码到底作了什么事情,大体过程以下:
一、虚拟机遇到new指令,到常量池定位到这个类的符号引用。
二、检查符号引用表明的类是否被加载、解析、初始化过。
三、虚拟机为对象分配内存。
四、虚拟机将分配到的内存空间都初始化为零值。
五、虚拟机对对象进行必要的设置。
六、执行方法,成员变量进行初始化。
七、将对象的引用指向这个内存区域。
咱们把这个过程简化一下,简化成3个步骤:
a、JVM为对象分配一块内存M
b、在内存M上为对象进行初始化
c、将内存M的地址复制给singleton变量
由于将内存的地址赋值给singleton变量是最后一步,因此Thread1在这一步骤执行以前,Thread2在对singleton==null进行判断一直都是true的,那么他会一直阻塞,直到Thread1将这一步骤执行完。
可是,以上过程并非一个原子操做,而且编译器可能会进行重排序,若是以上步骤被重排成:
a、JVM为对象分配一块内存M
c、将内存的地址复制给singleton变量
b、在内存M上为对象进行初始化
这样的话,Thread1会先执行内存分配,在执行变量赋值,最后执行对象的初始化,那么,也就是说,在Thread1尚未为对象进行初始化的时候,Thread2进来判断singleton==null就可能提早获得一个false,则会返回一个不完整的sigleton对象,由于他还未完成初始化操做。
这种状况一旦发生,咱们拿到了一个不完整的singleton对象,当尝试使用这个对象的时候就极有可能发生NPE异常。
那么,怎么解决这个问题呢?由于指令重排致使了这个问题,那就避免指令重排就好了。
因此,volatile就派上用场了,由于volatile能够避免指令重排。只要将代码改为如下代码,就能够解决这个问题:
1 public class Singleton {
2 private volatile static Singleton singleton;
3 private Singleton (){}
4 public static Singleton getSingleton() {
5 if (singleton == null) {
6 synchronized (Singleton.class) {
7 if (singleton == null) {
8 singleton = new Singleton();
9 }
10 }
11 }
12 return singleton;
13 }
14 }
复制代码
对singleton使用volatile约束,保证他的初始化过程不会被指令重排。
看到这里可能有朋友会问了,说到底上面问题仍是个有序性的问题,不是说synchronized是能够保证有序性的么,这里为何就不行了呢?
首先,能够明确的一点是:synchronized是没法禁止指令重排和处理器优化的。那么他是如何保证的有序性呢?
这就要再把有序性的概念扩展一下了。Java程序中自然的有序性能够总结为一句话:若是在本线程内观察,全部操做都是自然有序的。若是在一个线程中观察另外一个线程,全部操做都是无序的。
以上这句话也是《深刻理解Java虚拟机》中的原句,可是怎么理解呢?周志明并无详细的解释。这里我简单扩展一下,这其实和as-if-serial语义有关。
as-if-serial语义的意思指:无论怎么重排序,单线程程序的执行结果都不能被改变。编译器和处理器不管如何优化,都必须遵照as-if-serial语义。
这里不对as-if-serial语义详细展开了,简单说就是,as-if-serial语义保证了单线程中,无论指令怎么重排,最终的执行结果是不能被改变的。
那么,咱们回到刚刚那个双重校验锁的例子,站在单线程的角度,也就是只看Thread1的话,由于编译器会遵照as-if-serial语义,因此这种优化不会有任何问题,对于这个线程的执行结果也不会有任何影响。
可是,Thread1内部的指令重排却对Thread2产生了影响。
那么,咱们能够说,synchronized保证的有序性是多个线程之间的有序性,即被加锁的内容要按照顺序被多个线程执行。可是其内部的同步代码仍是会发生重排序,只不过因为编译器和处理器都遵循as-if-serial语义,因此咱们能够认为这些重排序在单线程内部可忽略。
本文从两方面论述了volatile的重要性以及不可替代性:
一方面是由于synchronized是一种锁机制,存在阻塞问题和性能问题,而volatile并非锁,因此不存在阻塞和性能问题。
另一方面,由于volatile借助了内存屏障来帮助其解决可见性和有序性问题,而内存屏障的使用还为其带来了一个禁止指令重排的附件功能,因此在有些场景中是能够避免发生指令重排的问题的。