多线程并发编程需要解决的几个问题是原子性,可见性和顺序性,今天我们主要分析一下原子性的底层实现原理及上层java实现原理。
首先介绍几个底层相关的术语:
CAS,CPU流水线,内存顺序冲突(伪共享),缓存行
-
处理器如何实现原子性
处理器如何保证多个处理器之间的原子性操作,注意处理器自己会保证基本内存操作的原子性(处理器从内存读读取或写入内存操作的原子性);本处讨论的多处理器系统,多处理器系统由于每个处理器都有自己的缓存,所以需要解决当一个处理器对内存写入数据时,其他处理器读取到最新的数据。
如果多个处理器同时对共享变量进行读改写操作,操作完成之后就会导致数据不一致,如何解决;
数据不一致的根本原因是各个处理器有自己的缓存系统,操作都是在自己的缓存中进行的,然后再刷新到内存中,所以解决方案就是一个CPU操作一个共享变量的时候其他CPU不能操作自己缓存系统中缓存了该共享变量内存地址的缓存。
一般处理器实现原子操作的实现原理有 总线加锁和缓存加锁两种方案。
-
使用总线锁保证原子性
所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞,那么该处理器可以独占共享内存
注意如果处理器进行内存操作的时候没有声言LOCK#信号,多个处理器并发操作内存的时候是基于总线总裁机制保证每个处理器操作内存的随机性和公平性
-
使用缓存锁保证原子性
在同一时刻,我们只需要保证对某个内存地址的操作是原子性即可,不需要锁住整个共享内存,由于总线锁定时,其他处理器对所有共享内存的操作都被阻塞,所以性能开销比较大;所以使用缓存锁定策略进行优化。
缓存锁定是指内存区域如果被缓存在处理器的缓存行中,并且在Lock操作期间被锁定,那么它执行锁操作回写内存的时候处理器不是在总线上声言LOCK#信号,而是修改内部的内存地址,并允许处理器自己的缓存一致性机制来保证操作的一致性(因为缓存一致性机制会阻止同时修改两个以上处理器缓存的内存区域来保证操作的原子性),因为这一保证,当一个处理器回写内存的时候,在自己缓存系统缓存行中缓存了相同内存的处理器的缓存行就会失效。
-
但是有两种情况处理器不会使用缓存锁定
第一种情况是:当操作的数据不能被缓存在处理器内部,或者操作的数据跨多个缓存行,处理器还是会锁总线
第二种情况是:有的处理器不支持缓存锁定。
-
-
Java如何实现原子操作
在Java中可以通过锁和循环CAS的方式来实现原子操作
-
使用循环CAS实现原子操作
JVM中的CAS操作正是利用了处理器提供的CMPXCHG指令实现的。自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止。
CAS存在的三个问题
- ABA问题,循环改变
- 循环实现长开销大
- 只能保证一个共享变量的原子操作—可以将多个共享变量和为一个进行操作,或使用JDK提供的AtomicReference原子类进行操作
-
使用锁机制实现原子操作
锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。JVM内部实现了很多锁机制,有偏向锁,轻量级锁和互斥锁。有意思的是除了偏向锁其他的锁底层的实现机制都是使用循环CAS,CAS获取锁,CAS释放锁。
-