java多线程编程(三)
原子操作的实现原理
1.术语定义
2.解决器如何实现原子操作
(1)使用总线锁保证原子性
第一个机制是通过总线锁保证原子性。假如多个解决器同时对共享变量进行读改写操作(i++就是经典的读改写操作),那么共享变量就会被多个解决器同时进行操作,这样读改写操作就不是原子的,操作完之后共享变量的值会和期望的不一致。举个例子,假如i=1,我们进行两次i++操作,我们期望的结果是3,但是有可能结果是2。
起因可能是多个解决器同时从各自的缓存中读取变量i,分别进行加1操作,而后分别写入系统内存中。那么,想要保证读改写共享变量的操作是原子的,就必需保证CPU1读改写共享变量的时候,CPU2不能操作缓存了该共享变量内存地址的缓存。
解决器使用总线锁就是来处理这个问题的。所谓总线锁就是使用解决器提供的一个LOCK#信号,当一个解决器在总线上输出此信号时,其余解决器的请求将被阻塞住,那么该解决器可以独占共享内存。
(2)使用缓存锁保证原子性
但是有两种情况下解决器不会使用缓存锁定。第一种情况是:当操作的数据不能被缓存在解决器内部,或者操作的数据跨多个缓存行(cache line)时,则解决器会调用总线锁定。第二种情况是:有些解决器不支持缓存锁定。对于Intel 486和Pentium解决器,就算锁定的内存区域在解决器的缓存行中也会调用总线锁定。针对以上两个机制,我们通过Intel解决器提供了很多Lock前缀的指令来实现。例如,位测试和修改指令:BTS、BTR、BTC;交换指令XADD、CMPXCHG,以及其余少量操作数和逻辑指令(如ADD、OR)等,被这些指令操作的内存区域就会加锁,导致其余解决器不能同时访问它。
3.Java如何实现原子操作
(1)使用循环CAS实现原子操作
(2)CAS实现原子操作的三大问题
1)ABA问题,就像一个人绕了远路还是走的原来的路吗? 不是 处理方案:加上版本号 2)循环时间长开销大: 自旋CAS假如长时间不成功,会给CPU带来非常大的执行开销。假如JVM能支持解决器提供的pause指令,那么效率会有肯定的提升。pause指令有两个作用:第一,它可以推迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,推迟的时间取决于具体实现的版本,在少量解决器上推迟时间是零;第二,它可以避免在退出循环的时候因内存顺序冲突(Memory Order Violation)而引起CPU流水线被清空(CPU Pipeline Flush),从而提高CPU的执行效率 3)只能保证一个共享变量的原子操作 当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候即可以用锁。还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比方,有两个共享变量i=2,j=a,合并一下ij=2a,而后用CAS来操作ij。从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,即可以把多个变量放在一个对象里来进行CAS操作。
(3)使用锁机制实现原子操作
锁机制保证了只有取得锁的线程才能够操作锁定的内存区域。JVM内部实现了很多种锁机制,有偏向锁、轻量级锁和互斥锁。有意思的是除了偏向锁,JVM实现锁的方式都用了循环CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时候使用循环CAS释放锁。