Java并发21-原子类:无锁工具类的典范


Java并发21-原子类:无锁工具类的典范

JDK提供了原子类,基本上是基于CAS原理实现的无锁下保证线程安全。

原子类组成概览图

无锁

无锁方案相对互斥锁方案,最大的好处就是性能

互斥锁方案为了保证互斥性,需要执行加锁、解锁操作,而加锁、解锁操作本身就消耗性能;

同时拿不到锁的线程还会进入阻塞状态,进而触发线程切换,线程切换对性能的消耗也很大。

而无锁方案则完全没有加锁、解锁的性能消耗,同时还能保证互斥性,相比互斥锁方案性能大大提升。

CAS

CASCompare 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类的源码来看CASJava中如何实现的。

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;
}

首先,通过thisoffset可以确定值的具体位置,delta是要增加的值。

代码先定位到值的位置,取出值作为oldValue,然后通过comapreAndSwap方法进行赋值。

其中参数thisoffset可以定位值的内存地址,传入oldValue作为期望值,传入oldValue+delta作为新值。

只有当内存地址的值和期望值相等时才会将新值赋值,并返回true,结束循环。否则,返回false并继续自旋进行赋值。

原子类概览

原子化的基本数据类型

AtomicBooleanAtomicIntegerAtomicLong 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)

原子化的对象引用类型

AtomicReferenceAtomicStampedReferenceAtomicMarkableReference,可以实现对象引用的原子化更新。

AtomicReference 提供的方法和原子化的基本数据类型差不多。

对象引用的更新需要重点关注 ABA 问题,AtomicStampedReferenceAtomicMarkableReference 这两个原子类可以解决 ABA 问题。

每次执行 CAS 操作,附加再更新一个版本号,只要保证版本号是递增的,那么即便 A 变成 B 之后再变回 A,版本号也不会变回来(版本号递增的)。

boolean compareAndSet(
  V expectedReference,
  V newReference,
  int expectedStamp,
  int newStamp) 

原子化数组

AtomicIntegerArrayAtomicLongArrayAtomicReferenceArray,这样可以原子化地更新数组里面的每一个元素。

这些类提供的方法和原子化的基本数据类型的区别是:每个方法多了一个数组的索引参数。


文章作者: Wendell
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Wendell !
评论
  目录