欢迎重视专栏【JAVA并发】

前语

JDK中供给了一系列的依据CAS实现的原子类,CAS 的全称是Compare-And-Swap,底层是lock cmpxchg指令,能够在单核和多核 CPU 下都能够确保比较交换的原子性。所以说,这些原子类都是线程安全的,而且是无锁并发,线程不会频繁上下文切换,所以在某些场景下功能是优于加锁。

本文就盘点一下JDK中的原子类,便利咱们后续拿来运用。

盘点JDK中基于CAS实现的原子类

基础原子类

  • AtomicInteger:Integer整数类型的原子操作类
  • AtomicBoolean: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):以原子办法将输入的数值与实例中的值相加并回来

运用:

盘点JDK中基于CAS实现的原子类

  • 成果1000,大致说明并发情况下确保了线程安全

原理分析:

全体实现思路: 自旋(循环) + CAS算法

  • 当旧的预期值 A == 内存值 V 此刻能够修正,将 V 改为 B
  • 当旧的预期值 A != 内存值 V 此刻不能修正,并从头获取现在的最新值,从头获取的动作便是自旋
public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
 }
  • valueOffset:偏移量表明该变量值相关于当时目标地址的偏移,Unsafe 便是依据内存偏移地址获取数据

盘点JDK中基于CAS实现的原子类

  • 从主内存中拷贝到作业内存中的值(每次都要从主内存拿到最新的值到本地内存),然后履行 compareAndSwapInt() 再和主内存的值进行比较,假定办法回来 false,那么就一直履行 while 办法,直到期望的值和实在值一样,修正数据。

盘点JDK中基于CAS实现的原子类

  • 原子类AtomicIntegervalue特点是volatile类型,确保了多线程之间的内存可见性,防止线程从作业缓存中获取失效的变量。

原子引证

原子引证主要是对目标的原子操作,原子引证类分为AtomicReferenceAtomicStampedReferenceAtomicMarkableReference。它们之间有什么区别呢?

  1. 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失利,有什么办法呢?

  1. 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();
    }
}

盘点JDK中基于CAS实现的原子类

  • 尽管目标的值变回了A,可是因为版本变了,所以主线程CAS失利
  1. 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();
    }
}

盘点JDK中基于CAS实现的原子类

  • 通过调用isMarked()办法检查是否发生改变。

原子数组

  • AtomicIntegerArray: Integer类型的原子数组
  • AtomicLongArray:Long类型的原子数组
  • AtomicReferenceArray:引证类型的原子数组

直接上比如

public class AtomicIntegerArrayTest {
    public static void main(String[] args) throws Exception{
        AtomicIntegerArray array = new AtomicIntegerArray(10);
        Thread t1 = new Thread(()->{
            int index;
            for(int i=1; i<100000; i++) {
                index = i%10; //范围0~9
                array.incrementAndGet(index);
            }
        });
        Thread t2 = new Thread(()->{
            int index;
            for(int i=1; i<100000; i++) {
                index = i%10; //范围0~9
                array.decrementAndGet(index);
            }
        });
        t1.start();
        t2.start();
        Thread.sleep(5 * 1000);
        System.out.println(array.toString());
    }
}

盘点JDK中基于CAS实现的原子类

  • 两个线程同时对数组目标进行加和减的操作,终究成果都是0,说明线程安全。

原子字段更新器

  • AtomicReferenceFieldUpdater
  • AtomicIntegerFieldUpdater
  • AtomicLongFieldUpdater

利用字段更新器,能够针对目标的某个域(Field)进行原子操作,只能配合 volatile 润饰的字段运用,不然会出现异常。

@Data
public class AtomicReferenceFieldUpdaterTest {
    private volatile int age = 10;
    private int age2;
    public static void main(String[] args) {
        AtomicIntegerFieldUpdater integerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(AtomicReferenceFieldUpdaterTest.class, "age");
        AtomicReferenceFieldUpdaterTest ref = new AtomicReferenceFieldUpdaterTest();
        // 对volatile 的age字段+1
        integerFieldUpdater.getAndIncrement(ref);
        System.out.println(ref.getAge());
        // 修正 非volatile的age2
        integerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(AtomicReferenceFieldUpdaterTest.class, "age2");
        integerFieldUpdater.getAndIncrement(ref);
    }
}

盘点JDK中基于CAS实现的原子类

  • 原子字段更新器只能更新volatile字段,它能够确保可见性,可是无法确保原子性。

原子累加器

原子累加器主要是用来做累加的,相关的类有LongAdderDoubleAdderLongAccumulatorDoubleAccumulator

LongAdder是jdk1.8中引入的,它的功能要比AtomicLong办法好。

LongAddr 类是 LongAccumulator 类的一个特例,只是 LongAccumulator 供给了更强壮的功能,能够自定义累加规则,当accumulatorFunction 为 null 时就等价于 LongAddr

这边做个功能的比照比如。

public class LongAdderTest {
    public static void main(String[] args) {
        System.out.println("LongAdder ...........");
        for (int i = 0; i < 5; i++) {
            addFunc(() -> new LongAdder(), adder -> adder.increment());
        }
        System.out.println("AtomicLong ...........");
        for (int i = 0; i < 5; i++) {
            addFunc(() -> new AtomicLong(), adder -> adder.getAndIncrement());
        }
    }
    private static <T> void addFunc(Supplier<T> adderSupplier, Consumer<T> action) {
        T adder = adderSupplier.get();
        long start = System.nanoTime();
        List<Thread> ts = new ArrayList<>();
        // 40个线程,每人累加 50 万
        for (int i = 0; i < 40; i++) {
            ts.add(new Thread(() -> {
                for (int j = 0; j < 500000; j++) {
                    action.accept(adder);
                }
            }));
        }
        ts.forEach(t -> t.start());
        ts.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        long end = System.nanoTime();
        System.out.println(adder + " cost:" + (end - start)/1000_000);
    }
}

盘点JDK中基于CAS实现的原子类

主要是因为LongAdder会设置多个累加单元,Therad-0 累加 Cell[0],而 Thread-1 累加Cell[1]… 最后将成果汇总。这样它们在累加时操作的不同的 Cell 变量,因此减少了 CAS 重试失利,然后提高功能。

总结

本文总结了JDK中供给的各种原子类,包含基础原子类、原子引证类、原子数组类、原子字段更新器和原子累加器等。有时候,运用这些原子类的功能是比加锁要高的,特别是在读多写少的场景下。可是,不知道我们发现没有,一切的原子类操作关于一个同享变量履行操作是原子的,假如关于多个同享变量操作时,循环 CAS 就无法确保操作的原子性,仍是老老实实加锁吧。

假如本文对你有协助的话,请留下一个赞吧

本文正在参与「金石方案 . 分割6万现金大奖」