盘点Java中基于CAS实现的原子类(上)

上一篇 / 下一篇  2022-12-06 11:03:12

  2022年度软件测试行业的趋势预测:如果你也想了解更多发展趋势,那就点击下方链接填写调查问卷吧!链接:http://vote.51testing.com/

  前言
  JDK中提供了一系列的基于CAS实现的原子类,CAS 的全称是Compare-And-Swap?,底层是lock cmpxchg指令,可以在单核和多核 CPU 下都能够保证比较交换的原子性。所以说,这些原子类都是线程安全的,而且是无锁并发,线程不会频繁上下文切换,所以在某些场景下性能是优于加锁。
  本文就盘点一下JDK中的原子类,方便我们后续拿来使用。
  基础原子类
  ·AtomicInteger:Integer整数类型的原子操作类
  · AtomicBooln:Boolean类型的原子操作类
  · AtomicLong:Long类型的原子操作类
  这边以AtomicInteger讲解下它的API和用法。
  构造方法:
  · public AtomicInteger()?:初始化一个默认值为 0 的原子型 Integer
  · public AtomicInteger(int initialValue)?:初始化一个指定值的原子型 Integer
  常用API:
  · public final int get(): 获取 AtomicInteger 的值
  · public final int getAndIncrement(): 以原子方式将当前值加 1,返回的是自增前的值
  · public final int incrementAndGet():以原子方式将当前值加 1,返回的是自增后的值
  · public final int getAndSet(int value):以原子方式设置为 newValue 的值,返回旧值
  · public final int addAndGet(int data):以原子方式将输入的数值与实例中的值相加并返回
  使用:
  结果1000,大致说明并发情况下保证了线程安全。
  原理分析:
  整体实现思路: 自旋(循环) + CAS算法
  · 当旧的预期值 A == 内存值 V 此时可以修改,将 V 改为 B
  · 当旧的预期值 A != 内存值 V 此时不能修改,并重新获取现在的最新值,重新获取的动作就是自旋
  public final int getAndIncrement() {
          return unsafe.getAndAddInt(this, valueOffset, 1);
   }
  valueOffset:偏移量表示该变量值相对于当前对象地址的偏移,Unsafe 就是根据内存偏移地址获取数据
  从主内存中拷贝到工作内存中的值(每次都要从主内存拿到最新的值到本地内存),然后执行compareAndSwapInt()再和主内存的值进行比较,假设方法返回 false,那么就一直执行 while 方法,直到期望的值和真实值一样,修改数据。
  原子类AtomicInteger的value?属性是volatile类型,保证了多线程之间的内存可见性,避免线程从工作缓存中获取失效的变量。
  原子引用
  原子引用主要是对对象的原子操作,原子引用类分为AtomicReference、AtomicStampedReference、AtomicMarkableReference。它们之间有什么区别呢?
  AtomicReference类
  普通的原子类对象
  public class AtomicReferenceDemo {
      public static void main(String[] args) {
          User user1 = new User("旭阳");
          // 创建原子引用包装类
          AtomicReference<User> atomicReference = new AtomicReference<>(user1);
          while (true) {
              User user2 = new User("alvin");
              // 比较并交换
              if (atomicReference.compareAndSet(user1, user2)) {
                  break;
              }
          }
          System.out.println(atomicReference.get());
      }
  }
  @Data
  @AllArgsConstructor
  @ToString
  class User {
      private String name;
  }
  调用compareAndSet()方法进行比较替换对象
  ABA问题
  但是如果使用AtomicReference类,会有一个ABA问题。什么意思呢?就是一个线程将共享变量从A改成B, 后面又改回A, 这是,另外一个线程就无法感知这个变化过程,就傻傻的比较,就以为没有变化,还是一开始的A,就替换了。 实际的确存在这样只要共享变量发生过变化,就要CAS失败,有什么办法呢?
  AtomicStampedReference类
  带版本号的原子类对象
  @Slf4j(topic = "a.AtomicStampedReferenceTest")
  public class AtomicStampedReferenceTest {
      // 构造AtomicStampedReference
      static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
      
      public static void main(String[] args) throws InterruptedException {
          log.debug("main start...");
          // 获取值 A
          String prev = ref.getReference();
          // 获取版本号
          int stamp = ref.getStamp();
          log.debug("版本 {}", stamp);
          // 如果中间有其它线程干扰,发生了 ABA 现象
          other();
          Thread.sleep(1000);
          // 尝试改为 C
          log.debug("change A->C {}", ref.compareAndSet(prev, "C", stamp, stamp + 1));
      }
      private static void other() throws InterruptedException {
          new Thread(() -> {
              log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B",
                      ref.getStamp(), ref.getStamp() + 1));
              log.debug("更新版本为 {}", ref.getStamp());
          }, "t1").start();
          Thread.sleep(500);
          new Thread(() -> {
              log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A",
                      ref.getStamp(), ref.getStamp() + 1));
              log.debug("更新版本为 {}", ref.getStamp());
          }, "t2").start();
      }
  }
  虽然对象的值变回了A,但是由于版本变了,所以主线程CAS失败
  AtomicMarkableReference 类
  其实有时候并不关心共享变量修改了几次,而是只要标记下是否发生过更改,是否加个标记即可,所以就有了AtomicMarkableReference类。
  @Slf4j(topic = "c.AtomicMarkableReferenceTest")
  public class AtomicMarkableReferenceTest {
      // 构造 AtomicMarkableReference, 初始标记为false
      static AtomicMarkableReference<String> ref = new AtomicMarkableReference<>("A", false);
      public static void main(String[] args) throws InterruptedException {
          log.debug("main start...");
          other();
          Thread.sleep(1000);
          // 看看是否发生了变化
          log.debug("change {}", ref.isMarked());
      }
      private static void other() throws InterruptedException {
          new Thread(() -> {
              log.debug("change A->B {}", ref.compareAndSet(ref.getReference(), "B",
                      false, true));
          }, "t1").start();
          Thread.sleep(500);
          new Thread(() -> {
              log.debug("change B->A {}", ref.compareAndSet(ref.getReference(), "A",
                      true, true));
          }, "t2").start();
      }
  }
  通过调用isMarked()方法查看是否发生变化。

TAG: 软件开发 Java java

 

评分:0

我来说两句

Open Toolbar