多线程编程核心技术(十六)原子类

并发编程的几个注意点,原子性一直是需要提到的。

在思想里面,一个是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算法原理

posted @ 2021-01-04 10:02  smartcat994  阅读(122)  评论(0编辑  收藏  举报