多线程编程核心技术(十六)原子类
并发编程的几个注意点,原子性一直是需要提到的。
在思想里面,一个是volatile实现可见性,或者加锁实现原子性。syn的本质其实是管程(也就是监视器),JVM层面的。Lock是JDK层面的,AQS实现
还有一个就是原子性。Java 里面提供的是Atomic家族。
原子性!=CAS,CAS是一种思想,一种乐观的思想。CAS=CompareAndSwaping。比较并且交换。
CAS伪代码:
首先计算 newValue = count+1,如果 cas(count,newValue) 返回的值不等于 count,则意味着线程在执行完代码①处之后,执行代码②处之前,count 的值被其他线程更新过。那此时该怎么处理呢?可以采用自旋方案,就像下面代码中展示的,可以重新读 count 最新的值来计算 newValue 并尝试再次更新,直到成功。
class SimulatedCAS{ volatile int count; // 实现count+=1 addOne(){ do { newValue = count+1; //① }while(count != cas(count,newValue) //② } // 模拟实现CAS,仅用来帮助理解 synchronized int cas( int expect, int newValue){ // 读目前count的值 int curValue = count; // 比较目前count值是否==期望值 if(curValue == expect){ // 如果是,则更新count的值 count= newValue; } // 返回写入前的值 return curValue; } }
一般CAS都自带一个自旋的循环尝试。其实也就是使用多次访问来保证这段时间内,数据是干净的,而且不会影响别的程序,对此造成的重写覆盖。无锁方案相对互斥锁方案,最大的好处就是性能。互斥锁方案为了保证互斥性,需要执行加锁、解锁操作,而加锁、解锁操作本身就消耗性能;同时拿不到锁的线程还会进入阻塞状态,进而触发线程切换,线程切换对性能的消耗也很大。
上面的伪代码是用了syn来提供原子性。但实际的Atomic里面使用的是硬件级的思想。unsafe来对JVM中C方法,C方法对操作系统,操作系统来对硬件资源。原因是本身CPU就是支持CAS指令的。
Atomic类中的基本数据类型:Integer,Long,Boolean的主要方法
getAndIncrement() //原子化i++ getAndDecrement() //原子化的i-- incrementAndGet() //原子化的++i decrementAndGet() //原子化的--i //当前值+=delta,返回+=前的值 getAndAdd(delta) //当前值+=delta,返回+=后的值 addAndGet(delta) //CAS操作,返回是否成功 compareAndSet(expect, update) //以下四个方法 //新值可以通过传入func函数来计算 getAndUpdate(func) updateAndGet(func) getAndAccumulate(x,func) accumulateAndGet(x,func)
其实本质还是Unsafe中的C方法
在Unsafe中的C底层的代码逻辑。也就是说其实还是需要用到强制刷新CPU的高级缓存,不过这个也是意料之内的,不然无法解决可见性问题。
#include <sun/misc/Unsafe.h>
static inline bool compareAndSwap (volatile jint *addr, jint old, jint new_val) { jboolean result = false; spinlock lock; // result=原先指针指向的地址的值(*addr)是否与旧的值(old)相等 if ((result = (*addr == old))) // 如果相等则把内存修改为新值 *addr = new_val; return result; }
原子化的对象引用类型
相关实现有 AtomicReference、AtomicStampedReference 和 AtomicMarkableReference,利用它们可以实现对象引用的原子化更新。
CAS其实会有个ABA问题,就是假设 count 原本是 A,线程 T1 在执行完代码①处之后,执行代码②处之前,有可能 count 被线程 T2 更新成了 B,之后又被 T3 更新回了 A,这样线程 T1 虽然看到的一直是 A,但是其实已经被其他线程更新过了。
这边利用时间的天然重复性低的特性或者哨兵可以完成。也就是加一个版本号。每次执行 CAS 操作,附加再更新一个版本号,只要保证版本号是递增的,那么即便 A 变成 B 之后再变回 A,版本号也不会变回来(版本号递增的)。AtomicStampedReference 实现的 CAS 方法就增加了版本号参数,方法签名如下:
boolean compareAndSet( V expectedReference, V newReference, int expectedStamp, int newStamp)
AtomicMarkableReference的区别之一就是它不会返回版本号,而是返回布尔值。
无锁方案相对于互斥锁方案,优点非常多,首先性能好,其次是基本不会出现死锁问题(但可能出现饥饿和活锁问题,因为自旋会反复重试)。Java 提供的原子类大部分都实现了 compareAndSet() 方法,基于 compareAndSet() 方法
参考文章:深入理解CAS算法原理