前言
之前我们通过volatiile关键字修饰一个变量i,可以使得在A线程中修改这个变量的副本之后能够立即刷新回内存,并且其他线程在读取之前必须要从主内存中读取这个值,从而达到了简单的线程间同步数据的目的
但是我们也看到这个机制并不是完整的,当访问的变量值依赖于这个变量的前值的时候,这个机制会失效,也就是volatile只解决变量的内存可见性,但是并没有解决原子性。
今天就来看看一个最常用的解决线程间同步的机制以及其背后的原理。
问题
继续我们之前的代码
当多个线程访问PlainSeller的实例时,ticket最终的值并不等于increment的调用次数,也就是说如果有10个线程,每个线程调用了一次increment,按理说ticket的值应该是10,但是结果中有时候会出现比10小的情况。
解决
只需要在increment方法前面加上synchronized关键字即可,我们大白话一下这里发生了什么。
当CPU在执行指令的时候,当遇到了synchronized修饰的方法时会去全局找一把钥匙,如果这个钥匙能够拿到,则能够继续执行,如果不能拿到,则需要等待,这样就确保了在同一个时刻只有一个线程在执行方法体中的指令,也就不会产生同时访问一个变量导致的主内存与线程本地缓存中值不一致的现象了。
如果方法中指令很多,但只有很小一部分代码在同时访问临界资源的时候,这个时候在方法上加synchronized就会很影响效率,因为不需要同步的指令也需要等待拿到钥匙之后才能执行,这个时候我们可以在代码块上加synchronized关键字,如下:
原理
首先看下两个increment的JVM指令:
SyncronizedSeller
PlainSeller
它俩唯一的区别就是flags上增加了ACC_SYNCHRONIZED位,区别并不明显,看不出来有什么差异。
我们再来看看对应的汇编指令,贴出synchronized部分指令如下:
这里跟不加synchronized关键字的区别在于多了很多 lock cmpxchg 指令。
那我们看一看这个指令是干啥的。
先看cmpxchg,这个指令是比较然后交换的意思,那么比较什么交换什么呢?简单说,多线程中有个叫CAS的原理或者算法,通过这个算法,可以让两个线程同步的访问同一个变量从而达到消除上述bug的作用,在x86指令集里面,CAS原理就是通过cmpxchg这条指令实现的,并且在多核CPU上这条指令需要加上lock前缀,所以就有了lock cmpxchg指令,syncronized通过这条指令的实现,从而达到了同步访问变量的目的。
那么这里就留下了一个坑:CAS原理。
小结
今天我们通过synchronized关键字解决了多个线程同时访问临界资源的时候带来的内存原子性问题,我们也通过背后的JVM指令和汇编指令了解其背后的原理,接下来我们继续看看还有哪些方法能够解决内存原子性问题。如果大家可以给通过crazy042438这个v号问我吧