Java并发21-原子类:无锁工具类的典范
JDK
提供了原子类,基本上是基于CAS
原理实现的无锁下保证线程安全。
无锁
无锁方案相对互斥锁方案,最大的好处就是性能。
互斥锁方案为了保证互斥性,需要执行加锁、解锁操作,而加锁、解锁操作本身就消耗性能;
同时拿不到锁的线程还会进入阻塞状态,进而触发线程切换,线程切换对性能的消耗也很大。
而无锁方案则完全没有加锁、解锁的性能消耗,同时还能保证互斥性,相比互斥锁方案性能大大提升。
CAS
CAS
即Compare And Swap
,就是比较并交换。CAS
是一种CPU指令,作为CPU指令,CAS
本身是可以保证原子性的,所以CAS
是通过硬件层面上的支持来保障数据更新的线程安全。
CAS
有3个参数,共享变量内存地址A
(旧值),用于比较值B
(期望值),共享变量新值C
(更新值)。只有当期望值B
和旧值A
相等时,才会将旧值更新为新值C
。
如何理解这句话?
设有一值v
=1,有多个线程同时对该值进行+1或-1操作。
CAS
指令会先把旧值取出来,计算得到新值,并将旧值赋值给期望值。因为在计算新值的过程中,可能其他的线程已经将原内存地址的值修改过,所以需要通过更新值与当前内存地址的值进行比较,只有当期望值与内存地址的值相同时,才会将新值进行赋值。若期望值和内存地址的值不相同,那么说明已经有线程修改过该值了。所以当前线程的修改就失败了,这样保证了线程安全。
ABA问题
接上文,若期望值和内存地址值相同。是否能说明该值未被其他线程修改过?
在计算新值期间,v
可能被加1又被减1,所以导致该值最终未发生变化,但实际上它是变化过的。这就是ABA
问题。
原子类源码
我们取一个原子类底层的unsafe
类的源码来看CAS
在Java
中如何实现的。
public final int getAndAddInt(Object this, long offset, int delta) {
int oldValue;
do {
oldValue = this.getIntVolatile(this, offset);
} while(!this.compareAndSwapInt(this, offset, oldValue, oldValue + delta));
return oldValue;
}
首先,通过this
和offset
可以确定值的具体位置,delta
是要增加的值。
代码先定位到值的位置,取出值作为oldValue
,然后通过comapreAndSwap
方法进行赋值。
其中参数this
和offset
可以定位值的内存地址,传入oldValue
作为期望值,传入oldValue+delta
作为新值。
只有当内存地址的值和期望值相等时才会将新值赋值,并返回true
,结束循环。否则,返回false
并继续自旋进行赋值。
原子类概览
原子化的基本数据类型
AtomicBoolean
、AtomicInteger
和 AtomicLong
API比较简单
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)
原子化的对象引用类型
AtomicReference
、AtomicStampedReference
和 AtomicMarkableReference
,可以实现对象引用的原子化更新。
AtomicReference
提供的方法和原子化的基本数据类型差不多。
对象引用的更新需要重点关注 ABA
问题,AtomicStampedReference
和 AtomicMarkableReference
这两个原子类可以解决 ABA
问题。
每次执行 CAS
操作,附加再更新一个版本号,只要保证版本号是递增的,那么即便 A
变成 B
之后再变回 A
,版本号也不会变回来(版本号递增的)。
boolean compareAndSet(
V expectedReference,
V newReference,
int expectedStamp,
int newStamp)
原子化数组
AtomicIntegerArray
、AtomicLongArray
和 AtomicReferenceArray
,这样可以原子化地更新数组里面的每一个元素。
这些类提供的方法和原子化的基本数据类型的区别是:每个方法多了一个数组的索引参数。