今天来聊一聊Java并发编程中两个经常使用的关键字:volatile和synchronized。在介绍这两个关键字以前,首先要搞明白并发编程中的两个问题:java
线程之间是如何通讯的程序员
线程之间如何同步编程
Java内存模型
Java线程的通讯由Java内存模型(JMM)控制,Java内存模型的抽象如图:微信
Java线程之间的通讯老是隐式进行,通讯过程对程序员彻底透明。多个线程经过读-写共享内存来实现通讯。网络
图中线程A与线程B通讯的具体步骤是:多线程
线程A把更新过的共享变量刷新到主内存中并发
线程B从主内存读取共享变量ide
例如,共享变量x的初始值为0,线程A将x修改成1(x=x+1),线程B读取到的x就是1,对于程序员来说,就是线程A给线程B发消息说它把x的值更新为1。性能
第一个问题搞明白了,再思考一下第二个问题。线程之间如何同步?在并发编程中,有三个重要的概念:原子性、可见性、一致性。atom
原子性
在Java中,对基本数据类型的读取和赋值操做都属于原子操做。
x = 10;
x = x + 1;
上面两条语句中,第一句是原子操做,而第二句不是,为何呢?实际上,第二句代码被编译为3条指令:
从内存中取x的值
x+1操做
计算结果存入内存
可见性
当多个线程访问同一变量时,若是有一个线程修改了这个变量,那么其余线程马上能够看到修改后的值。
有序性
CPU执行指令是按照前后顺序执行的,可是指令的顺序并不必定等同于代码的顺序,编译器编译过程当中,为了提升性能,经常进行指令重排序。这种重排序不会改变单线程的语义,也就是说,你写的一段代码若是是单线程执行,编译器可能对执行进行重排序,但不论如何排序,最后获得的结果都是相同的。
另外,若是存在数据依赖性,编译器不会改变依赖关系的执行顺序。数据依赖性是指两个操做访问同一个变量,其中一个是写操做,那么这两个操做就有数据依赖性。
重排序对应多线程有哪些影响呢,咱们经过一段代码来看一下:
class ReorderExample {
int a = 0;
boolean flag = false;
public void writer() {
a = 1; // 1
flag = true; // 2
}
Public void reader() {
if (flag) { // 3
int i = a * a; // 4
……
}
}
}
上述代码中,flag是变量a被初始化的标识,若是此时有两个线程A和B,A执行writer()方法,B执行reader()方法。因为1和二、3和4不存在数据依赖性,那么就有可能出现这种状况:
A先执行语句2
B执行了语句3和4
A执行语句1
最终的结果并非咱们想要的,此时,重排序破坏了语义。
线程同步
对于上面所说的线程同步问题如何避免呢?可使用Java中的volatile和synchronized这两个关键字。
volatile
volatile关键字比较轻量级,只能够修饰变量。volatile修饰的变量,若是值被更新,会当即刷新主内存,而读volatile修饰的变量时,JMM会把线程对应的本地内存置为无效,从主内存中读取。这样volatile就能够保证线程的可见性。
volatile关键字在必定程度上能够保证有序性:
当第二个操做是volatile写时,不能进行重排序
当第一个操做是volatile读时,不能进行重排序
当第一个操做是volatile写,第二个操做是volatile读时,不能重排序
为了实现这些语义,JMM采用屏障插入策略:
在volatile写操做前插入StoreStore屏障,后面插入StoreLoad屏障
在volatile读操做后面插入LoadLoad屏障和LoadStore屏障
也就是说,volatile写操做前的全部写操做都必须执行完,且须要等到volatile写操做执行后才能执行读操做。volatile读操做执行完以后才能够进行其余操做。
能够把volatile当作一个屏障,其前面的操做不能放到volatile操做后面,后面的操做也不能放到volatile操做前面。
synchronized
synchronized比较重量级,能够用来修饰方法。synchronized关键字是给修饰对象加锁,只有得到锁的线程才能够执行,执行完后释放锁。所以synchronized保证了原子性和可见性。
本文分享自微信公众号 - 代码洁癖患者(Jackeyzhe2018)。
若有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一块儿分享。