原子类无锁并发利器

332 阅读2分钟

原子类无锁并发利器

前言引入

当存在如下场景,两个线程同时去将count值累加一万次,那么如下代码是否存在线程安全问题呢?

 public class Test2 {
     public static void main(String[] args) throws InterruptedException {
         TestCount testCount = new TestCount();
         Thread T1 = new Thread(()->{
             testCount.add10k();
         });
 ​
         Thread T2 = new Thread(()->{
             testCount.add10k();
         });
 ​
         T1.start();
         T2.start();
         T1.join();
         T2.join();
         System.out.println(testCount.count);
     }
 }
 ​
 class TestCount{
     long count = 0;
     void add10k(){
         int idx = 0;
         while (idx++ < 10000){
             count += 1;
         }
     }
 }

显然是存在的,最终的结果显然是小于两万的,原因是count值存在可见性问题,count+=1也存在原子性的问题。

一般思路都是将count采用volatile修饰,count+=1原子性问题采用synchronized互斥锁解决。

 class TestCount{
     volatile long count = 0;
     synchronized void add10k(){
         int idx = 0;
         while (idx++ < 10000){
             count += 1;
         }
     }
 }

但是对于这种简单的互斥操作,需要采用synchronized这种比较重的互斥锁吗?有没有更优的解决办法呢?当然存在,采用原子类无锁方案能够极大的提升性能

 class TestCount{
     AtomicLong count = new AtomicLong(0);
      void add10k(){
         int idx = 0;
         while (idx++ < 10000){
             count.getAndIncrement();
         }
     }
 }

原子类同样能够解决互斥性问题、原子性问题除此之外,因为原子类是无锁操作,没有用互斥锁解决带来的加锁解决性能消耗,这种绝佳方案是怎么做到的呢?

无锁方案实现原理

无锁方案之所以能够保证原子性,主要还是硬件保证,CPU为了解决并发问题,提供了CAS(Compare And Swap)指令即比较并交换,CAS一般包含三个参数,共享变量的内存地址A,用于比较的期望值B,更新共享变量C,当共享变量的内存地址A的值和共享变量B的值相等时,才将共享变量的内存地址A处的值更新为共享变量C。

将场景语义化如下

 class SimpleCAS{
     int count;
     public synchronized int cas(int expect,int newCount){
         // 读取count值
        int oldcount = count;
        // 读取的count值和期望值比较
        if (oldcount == expect){
            count = newCount;
        }
        // 返回老值
        return oldcount;
     }
 }

CAS指令判断并不是一次性的,如果比较失败又会重新取最新的值和期望值判断直到成功。

 class SimpleCAS{
     volatile int count;
     public synchronized int cas(int expect,int newCount){
         // 读取count值
        int oldcount = count;
        // 读取的count值和期望值比较
        if (oldcount == expect){
             count = newCount;
         }
        // 返回老值
        return oldcount;
     }
 ​
     // 自旋操作,执行cas方法
     public void addOne(){
         int newCount = 0;
         do {
             newCount = count + 1;
         }while (count != cas(count,newCount));
     }
 }

ABA问题

原子类虽然好用,但是一定需要的坑就是ABA问题,假如存在共享变量A值为5,线程T1将共享变量A的值改为2,而线程T2将共享变量A改为3,线程T3又将共享变量A改为2,那么对于线程T1来讲共享变量A是没有变的吗?显然不是,可能大多数场景我们并不关心ABA问题,对于基础数据递增可能认为值不变就够了,并不关心值是否已经修改,但是对于引用类型呢,这就一定要注意ABA问题,两个A虽然相等,但是属性可能已经发生变化。

原子类提供工具类解决ABA问题AtomicStampedReference和AtomicMarkableReference

 public static void main(String[] args) throws InterruptedException {
     // 初始化原子类 定义初始引用和标识
     // AtomicMarkableReference同理可得,只是将版本戳换成了boolean类型
     AtomicStampedReference<String> reference = new AtomicStampedReference<>("zhangsan",1001);
     /**
       * expectedReference 期望的引用
       * newReference     新的引用
       * expectedStamp    期望的版本戳
       * newStamp         新的版本戳
       * 只有当期望引用和期望版本戳都符合实际版本戳和引用才能替换成功
     */
     reference.compareAndSet("zhangsan","lisi",1002,1003);
     // zhangsan 替换失败的原因是期望版本戳和实际版本戳不匹配
     System.out.println(reference.getReference());
     reference.compareAndSet("zhangsan","lisi",1001,1002);
     // lisi 替换成功
     System.out.println(reference.getReference());
     reference.compareAndSet("lisi1","wangwu",1002,1003);
     // lisi 替换失败的原因是期望引用和实际引用不匹配
     System.out.println(reference.getReference());
 }

getAndIncrement源码分析

 /**
   * this 指当前对象
   * valueOffset 指内存地址偏移量
 */
 public final int getAndIncrement() {
     return unsafe.getAndAddInt(this, valueOffset, 1);
 }
 /**
   * this 指当前对象
   * valueOffset 指内存地址偏移量
 */
 public final int getAndAddInt(Object var1, long var2, int var4) {
     int var5;
     do {
         // 就是读取主内存的值
         var5 = this.getIntVolatile(var1, var2);
         // this.compareAndSwapInt方法就是对应上诉的cas方法,不过返回值是boolean类型
         // var5读取的主内存值
         // 更新成功返回true,跳出循环
     } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
 ​
     return var5;
 }

原子工具类总览

原子工具类是一个大家族,根据使用可以分为原子化基本数据类型、原子化的对象引用类型、原子化数组、原子化对象属性更新器和原子化的累加器,方法都基本类似不需要刻意去记,需要用到的时候再来查就可以,但是需要有个印象,如下图所示。

image-20220302143728329

\