该并发学习系列以阅读《Java并发编程的艺术》一书的笔记为蓝本,汇集一些阅读过程中找到的解惑资料而成。这是一个边看边写的系列,有兴趣的也可以先自行购买此书学习。
本文首发:windCoder
处理器中的原子操作
原子操作意为:不可中断的一个或一系列操作。现在先了解几个相关术语:
术语名称 | 英文 | 解释 |
---|---|---|
比较并交换 | Compare And Sqap | CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较旧值有没有发生变化,如果没有发生变化,才交换新值,发生了变化则不交换。 |
CPU流水线 | CPU Pipeline | CPU流水线的工作方式就像工业生产上的装配流水线,在CPU中由56个不同功能的电路单元组成一条指令处理流水线,然后将一条x86指令分成56步后再由这些电路单元分别执行,这样就能实现在一个CPU时钟周期完成一条指令,因此提高CPU的运算速度。 |
内存顺序周期 | Memory Order Violation | 内存顺序冲突一般是由假共享引起的,假共享是指多个CPU同时修改同一个缓存行的不同部分而引起其中一个CPU的操作无效,当出现这个内存顺序冲突时,CPU必须清空流水线。 |
处理器使用对缓存加锁或总线加锁的方式来实现多处理器之间的原子操作。
处理器会自动保证基本的内存操作的原子性。处理器保证从系统内存中读取或者写入一个字节是原子的,即:当一个处理器读取一个字节时,其他处理器不能访问这个字节的内存地址。
处理器不能自动保证复杂的内存操作的原子性。如:跨总线宽度、跨多个缓存行和跨页表的访问等复杂内存操作。
处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。
总线锁定就是使用处理器提供的一个#LOCK信号,当一个处理器在总线程上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。
总线锁定把CPU和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,目前处理器在某些场合下使用缓存锁定代替总线锁定来进行优化。
缓存锁定是指内存区域如果被缓存在处理器的缓存行中,并且在LOCK操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声言#LOCK信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性。
该缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其它处理器回写已被锁定的缓存行的数据时,会使缓存行无效。
但有两种情况下处理器不会使用缓存锁定:
- 1.当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行时,处理器会调用总线锁定。
- 2.有些处理器不支持缓存锁定。如Intel 486和Pentium处理器,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定。
Java中原子操作
Java中可以通过锁和循环CAS的方式来实现原子操作。
使用循环CAS实现原子操作
JVM中使用的CAS操作利用了处理器提供的CMPXCHG
指令实现的。
自旋CAS实现的基本思想是循环进行CAS操作直到成功为止。
从 Java 1.5开始,JDK的并发包里提供了一些类来支持原子操作,如AtomicBoolean(用原子方式更新的boolean值)、AtomicInteger(用原子方式更新的int值)和AtomicLong(用原子方式更新的long值)。这些原子包装类还提供了有用的工具方法,如以原子的方式将当前值自增1和自减1。
CAS实现原子操作有三大问题:ABA问题、循环时间长开销大、只能保证一个共享变量的原子性。
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。若JVM能支持处理器提供的pause指令,效率会有一定的提升。
pause指令可以延迟流水线执行指令(de-pipline),也可以避免在退出循环的时候因内存顺序冲突(Memory Order Violation)而引起CPU流水线被清空(CPU Pipelne Flush),从而提高CPU的执行效率。
更多CAS的知识可参考:CAS初探
使用锁机制实现原子操作
锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。
JVM内部实现了很多种锁机制,有偏向锁、轻量级锁和互斥锁。
除了偏向锁,JVM实现锁的方法都使用了CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块时使用循环CAS释放锁。
Java 中大部分容器和框架依赖于 volatile 和原子操作的实现原理,了解这些原理对我们进行并发编程会很有帮助。