5.1 并发的三大特性
5.1.1 原子性(Atomicity):
Java中,对基本数据类型的读取和赋值操作是原子性操作,所谓原子性操作就是指这些操作是不可中断的,要做一定做完,要么就没有执行。 比如:i = 2; //原子操作
j = i; //非原子操作 1.取i的值,2.赋值给j
i++; //非原子操作 1.i自增,2.将i写入内存
i = i + 1; //同上
只有简单的读取,赋值是原子操作,还只能是用数字赋值,用变量的话还多了一步读取变量值的操作。有个例外是,虚拟机规范中允许对64位数据类型(long和double),分为2次32为的操作来处理,但是最新JDK实现还是实现了原子操作的。
JMM只实现了基本的原子性,像上面i++那样的操作,必须借助于synchronized和Lock来保证整块代码的原子性了。线程在释放锁之前,必然会把i的值刷回到主存的。
5.1.2 可见性(Visibility):
当一个变量被volatile修饰时,那么对它的修改会立刻刷新到主存,当其它线程需要读取该变量时,会去内存中读取新值。而普通变量则不能保证这一点。其实通过synchronized和Lock也能够保证可见性,线程在释放锁之前,会把共享变量值都刷回主存,但是synchronized和Lock的开销都更大。
5.1.3 有序性(Ordering):
JMM是允许编译器和处理器对指令重排序的,但是规定了as-if-serial语义,即不管怎么重排序,程序的执行结果不能改变。比如下面的程序段:double pi = 3.14; //A
double r = 1; //B
double s= pi * r * r;//C
上面的语句,可以按照A->B->C执行,结果为3.14,但是也可以按照B->A->C的顺序执行,因为A、B是两句独立的语句,而C则依赖于A、B,所以A、B可以重排序,但是C却不能排到A、B的前面。JMM保证了重排序不会影响到单线程的执行,但是在多线程中却容易出问题。
5.2 JAVA内存模型
每一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。当线程访问某一个对象时候值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。 下面一幅图:5.3 volatile关键字(不具备原子性)
5.3.1 volatile关键字的两层语义
一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
- 禁止进行指令重排序。
5.3.2 volatile的工作
volatile关键字的作用是强制从公共堆栈中取得变量的值,而不是从线程私有数据栈中取得变量的值。
共享内存是在主存中,读取的时候考虑性能可能 CPU 会将这块共享内存复制到缓存中(多核处理器每个 CPU 都有一个缓存),由于工作线程访问该共享内存的时候,先从缓存中获取,如果获取不到再从主存中获取,如下图所示:
线程在工作内存中修改完共享变量后,会将其值同步回共享变量所在主存区域,此时需要访问主存,但是如果这块主存缓存过的话,并不会把值写回内存,而是写入缓存;对于其他 CPU 上的线程来说,如果需要访问 flag ,也会直接先从自己的缓存中获取该变量的值,即使该 flag 的值已经改变,其他线程读取到的也只能是自己缓存中已过期的值。如果加了 volatile 关键字,那么对该变量的读会直接从主存中读取,对该变量的写也直接写入主存,相当于使缓存无效。
5.3.2 volatile和synchronized对比
- 关键字volatile是轻量级的实现(利用汇编的lock),所以其效率要高于synchronized,并且volatile只能修饰变量,而synchronized可以修饰方法,以及代码块。但是随着新版的JDK的发布,synchronized关键字在执行效率上得到了很大的提升,在开发中使用synchronized的比例还是比较大的。
- 多线程访问volatile不会阻塞,但是synchronized会出现阻塞。
- volatile能保证数据的可见性,但是不能保证原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为他会将私有内存和公有内存中的数据同步。
- volatile解决的是变量在多线程之间的可见性,而synchronized关键字解决的是多个线程之间访问资源的同步性。
5.4 扩展
5.4.1 volatile为什么不能保证原子性呢?
以volatile int i = 10;i++;为例分析:
i++实际为load、Increment、store三个操作。
某一时刻线程1将i的值load取出来,放置到cpu缓存中,然后再将此值放置到寄存器A中,然后A中的值自增1(寄存器A中保存的是中间值,没有直接修改i,因此其他线程并不会获取到这个自增1的值)。如果在此时线程2也执行同样的操作,获取值i10,自增1变为11,然后马上刷入主内存。此时由于线程2修改了i的值,实时的线程1中的i10的值缓存失效,重新从主内存中读取,变为11。接下来线程1恢复。将自增过后的A寄存器值11赋值给cpu缓存i。这样就出现了线程安全问题。
5.4.2 synchronized相对于volatile又是如何保证原子性呢?
-
volatile:从最终汇编语言从面来看,volatile使得每次将i进行了修改之后,增加了一个内存屏障lock addl $0x0,(%rsp)保证修改的值必须刷新到主内存才能进行内存屏障后续的指令操作。但是内存屏障之前的指令并不是原子的。
-
synchronized:则是使用lock cmpxchg %rsi,(%rdi)的原子指令,使得修改是原子操作。如果修改失败,则继续尝试,直到成功。