JUC原子类:ABA问题、原子类详解

1,097 阅读9分钟

这是我参与8月更文挑战的第2天,活动详情查看:8月更文挑战

往期推荐

一、Java中的13个原子操作类

在多线程环境下执行i++这个操作,并不能保证变量i的线程安全性。因为i++其实不是一个原子操作,i++是由以下3个步骤组成的:

  • (1)取出变量i的值。
  • (2)执行累加操作。
  • (3)累加后的结果写回变量i。 在多线程竞争环境下,以上3个步骤可能被不同的线程按照不同的顺序执行,因此无法保证在多线程环境下变量i的线程安全。在这种场景下,使用synchronized/lock等加锁方式来保证代码块互斥访问可以实现变量线程安全。

除了加锁方式外,Java提供的13个原子操作类也可以解决上述问题。Java中的13个原子操作类都是基于无锁方案实现的。与加锁的方案相比,原子操作类并没有加锁解锁和线程切换的消耗。 java.util.concurrent.atomic包提供了多类用法简单、性能高效、线程安全的原子操作类。主要包含以下四种类型:

  • 原子更新基本类型
  • 原子更新数组
  • 原子更新引用
  • 原子更新属性(字段)

1. 原子更新基本类型

其中原子更新基本类型主要是如下三个类:

  • AtomicBoolean类用于原子性地更新布尔类型。
  • AtomicInteger类用于原子性地更新整数类型。
  • AtomicLong类用于原子性地更新长整型类型。

以AtomicInteger案例代码:

import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerTest {

    public static void main(String[] args) {
        
        int temvalue = 0;
        AtomicInteger i = new AtomicInteger(0);
        temvalue = i.getAndSet(3);
        System.out.println("temvalue:" + temvalue + ";  i:" + i);//temvalue:0;  i:3
        temvalue = i.getAndIncrement();
        System.out.println("temvalue:" + temvalue + ";  i:" + i);//temvalue:3;  i:4
        temvalue = i.getAndAdd(5);
        System.out.println("temvalue:" + temvalue + ";  i:" + i);//temvalue:4;  i:9
    }

}

2. 原子更新数组

原子更新数组指的是以原子的方式更新数组中的某个索引位置的元素,主要包括以下3个类:

  • AtomicIntegerArray类用于原子性地更新整数类型的数组。

  • AtomicLongArray类用于原子性地更新长整型类型的数组。

  • AtomicReferenceArray类用于原子性地更新引用类型的数组。 其中常用的方法有:

  • int addAndGet(int i, int delta):将输入值和对应索引i位置的元素相加。

  • boolean compareAndSet(int i, int expect, int update):如果当前值等于预期值,则将数组索引为i位置的元素设置为update值。 举个AtomicIntegerArray例子:

import java.util.concurrent.atomic.AtomicIntegerArray;

public class Demo {
    public static void main(String[] args) throws InterruptedException {
        AtomicIntegerArray array = new AtomicIntegerArray(new int[] { 0, 0 });
        System.out.println(array);
        System.out.println(array.getAndAdd(1, 2));
        System.out.println(array);
    }
}


输出结果:
[0, 0]
0
[0, 2]

下面看一下compareAndSet()的源码实现,它的源码如下:

public final boolean compareAndSet(int i, int expect, int update) {
    return compareAndSetRaw(checkedByteOffset(i), expect, update);
}

private boolean compareAndSetRaw(long offset, int expect, int update) {
    return unsafe.compareAndSwapInt(array, offset, expect, update);
}

最终实现上还是调用了Unsafe的compareAndSwapInt()

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

3. 原子更新引用类型

原子更新引用类型包括如下三个类:

  • AtomicReference:更新引用类型。
  • AtomicReferenceFieldUpdate :更新引用类型中的字段。
  • AtomicMarkableReference:更新带有标记位的引用类型,可以更新一个布尔类型的标记位和引用类型。

这三个类提供的方法都差不多,首先构造一个引用对象,然后把引用对象set进Atomic类,然后调用compareAndSet等一些方法去进行原子操作,原理都是基于Unsafe实现

举个AtomicReference例子:

import java.util.concurrent.atomic.AtomicReference;

public class AtomicReferenceTest {
    
    public static void main(String[] args){

        // 创建两个Person对象,它们的id分别是101和102。
        Person p1 = new Person(101);
        Person p2 = new Person(102);
        // 新建AtomicReference对象,初始化它的值为p1对象
        AtomicReference ar = new AtomicReference(p1);
        // 通过CAS设置ar。如果ar的值为p1的话,则将其设置为p2。
        ar.compareAndSet(p1, p2);

        Person p3 = (Person)ar.get();
        System.out.println("p3 is "+p3);
        System.out.println("p3.equals(p1)="+p3.equals(p1));
    }
}

class Person {
    volatile long id;
    public Person(long id) {
        this.id = id;
    }
    public String toString() {
        return "id:"+id;
    }
}

结果输出:

p3 is id:102
p3.equals(p1)=false

结果说明:

  • 新建AtomicReference对象ar时,将它初始化为p1。
  • 紧接着,通过CAS函数对它进行设置。如果ar的值为p1的话,则将其设置为p2。
  • 最后,获取ar对应的对象,并打印结果。p3.equals(p1)的结果为false,这是因为Person并没有覆盖equals()方法,而是采用继承自Object.java的equals()方法;而Object.java中的equals()实际上是调用"=="去比较两个对象,即比较两个对象的地址是否相等。

4. 原子更新字段类

原子更新字段类自然是用于更新某个类中的字段值,主要包含如下四个类:

  • AtomicIntegerFieldUpdater:更新整型字段的更新器。

  • AtomicLongFieldUpdater:更新长整型字段的更新器。

  • AtomicStampedFieldUpdater: 原子更新带有版本号的引用类型。

  • AtomicStampedReference:更新带有版本号的引用类型,需要更新版本号和引用类型,主要为了解决ABA问题。 使用步骤为:

  • 第一步,因为原子更新字段类都是抽象类,每次使用的时候必须使用静态方法newUpdater()创建一个更新器,并且需要设置想要更新的类和属性

  • 第二步,更新类的字段必须使用public volatile修饰。 举个例子:

public class TestAtomicIntegerFieldUpdater {

    public static void main(String[] args){
        TestAtomicIntegerFieldUpdater tIA = new TestAtomicIntegerFieldUpdater();
        tIA.doIt();
    }

    public AtomicIntegerFieldUpdater<DataDemo> updater(String name){
        return AtomicIntegerFieldUpdater.newUpdater(DataDemo.class,name);

    }

    public void doIt(){
        DataDemo data = new DataDemo();
        System.out.println("publicVar = "+updater("publicVar").getAndAdd(data, 2));
        /*
            * 由于在DataDemo类中属性value2/value3,在TestAtomicIntegerFieldUpdater中不能访问
            * */
        //System.out.println("protectedVar = "+updater("protectedVar").getAndAdd(data,2));
        //System.out.println("privateVar = "+updater("privateVar").getAndAdd(data,2));

        //System.out.println("staticVar = "+updater("staticVar").getAndIncrement(data));//报java.lang.IllegalArgumentException
        /*
            * 下面报异常:must be integer
            * */
        //System.out.println("integerVar = "+updater("integerVar").getAndIncrement(data));
        //System.out.println("longVar = "+updater("longVar").getAndIncrement(data));
    }

}

class DataDemo{
    public volatile int publicVar=3;
    protected volatile int protectedVar=4;
    private volatile  int privateVar=5;

    public volatile static int staticVar = 10;
    //public  final int finalVar = 11;

    public volatile Integer integerVar = 19;
    public volatile Long longVar = 18L;

}

再说下对于AtomicIntegerFieldUpdater 的使用稍微有一些限制和约束,约束如下:

  • 字段必须是volatile类型的,在线程之间共享变量时保证立即可见。
  • 字段的描述类型(修饰符public/protected/default/private)是与调用者与操作对象字段的关系一致。也就是说调用者能够直接操作对象字段,那么就可以反射进行原子操作。但是对于父类的字段,子类是不能直接操作的,尽管子类可以访问父类的字段。
  • 只能是实例变量,不能是类变量,也就是说不能加static关键字。
  • 只能是可修改变量,不能使final变量,因为final的语义就是不可修改。实际上final的语义和volatile是有冲突的,这两个关键字不能同时存在。
  • 对于AtomicIntegerFieldUpdaterAtomicLongFieldUpdater只能修改int/long类型的字段,不能修改其包装类型(Integer/Long)。如果要修改包装类型就需要使用AtomicReferenceFieldUpdater

二、ABA问题

ABA问题发生在多线程环境中,当某线程连续读取同一块内存地址两次,两次得到的值一样,它简单地认为“此内存地址的值并没有被修改过”,然而,同时可能存在另一个线程在这两次读取之间把这个内存地址的值从A修改成了B又修改回了A,这时还简单地认为“没有修改过”显然是错误的。

比如,两个线程按下面的顺序执行:

(1)线程1读取内存位置X的值为A;

(2)线程1阻塞了;

(3)线程2读取内存位置X的值为A;

(4)线程2修改内存位置X的值为B;

(5)线程2修改又内存位置X的值为A;

(6)线程1恢复,继续执行,比较发现还是A把内存位置X的值设置为C;

6.png 可以看到,针对线程1来说,第一次的A和第二次的A实际上并不是同一个A。

三、AtomicStampedReference解决ABA问题

AtomicStampedReference主要维护包含一个对象引用以及一个可以自动更新的整数"stamp"的pair对象来解决ABA问题。

3.1 AtomicStampReference类的属性

/*
*volatile修饰的pair
*/
private volatile Pair<V> pair;

3.2 AtomicStampReference类的构造方法

/**
  * V initialRef :任意类型的初始引用对象
  * int initialStamp :Int类型的初始版本号
  */
public class AtomicStampedReference<V> {
    private static class Pair<T> {
        final T reference;  //维护对象引用
        final int stamp;  //用于标志版本
        private Pair(T reference, int stamp) {
            this.reference = reference;
            this.stamp = stamp;
        }
        static <T> Pair<T> of(T reference, int stamp) {
            return new Pair<T>(reference, stamp);
        }
    }
  

3.3 AtomicStampReference的内部类Pair

private static class Pair<T> {
    //引用
    final T reference;
    //版本号
    final int stamp;
    //构造方法
    private Pair(T reference, int stamp) {
        this.reference = reference;
        this.stamp = stamp;
    }
    //生成一个Pair
    static <T> Pair<T> of(T reference, int stamp) {
        return new Pair<T>(reference, stamp);
    }
}

3.4 comparedAndSwap()方法

    /**
      * expectedReference :更新之前的原始值
      * newReference : 将要更新的新值
      * expectedStamp : 期待更新的标志版本
      * newStamp : 将要更新的标志版本
      */
    public boolean compareAndSet(V   expectedReference,
                             V   newReference,
                             int expectedStamp,
                             int newStamp) {
        // 获取当前的(元素值,版本号)对
        Pair<V> current = pair;
        return
            // 引用没变
            expectedReference == current.reference &&
            // 版本号没变
            expectedStamp == current.stamp &&
            // 新引用等于旧引用
            ((newReference == current.reference &&
            // 新版本号等于旧版本号
            newStamp == current.stamp) ||
            // 构造新的Pair对象并CAS更新
            casPair(current, Pair.of(newReference, newStamp)));
    }

    private boolean casPair(Pair<V> cmp, Pair<V> val) {
        // 调用Unsafe的compareAndSwapObject()方法CAS更新pair的引用为新引用
        return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
    }
  • 如果元素值和版本号都没有变化,并且和新的也相同,返回true;
  • 如果元素值和版本号都没有变化,并且和新的不完全相同,就构造一个新的Pair对象并执行CAS更新pair。

可以看到,java中的实现跟我们上面讲的ABA的解决方法是一致的。

  • 首先,使用版本号控制;
  • 其次,不重复使用节点(Pair)的引用,每次都新建一个新的Pair来作为CAS比较的对象,而不是复用旧的;
  • 最后,外部传入元素值及版本号,而不是节点(Pair)的引用。

7.png

案例:

private static AtomicStampedReference<Integer> atomicStampedRef =
        new AtomicStampedReference<>(1, 0);
public static void main(String[] args){
    Thread main = new Thread(() -> {
        System.out.println("操作线程" + Thread.currentThread() +",初始值 a = " + atomicStampedRef.getReference());
        int stamp = atomicStampedRef.getStamp(); //获取当前标识别
        try {
            Thread.sleep(1000); //等待1秒 ,以便让干扰线程执行
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        boolean isCASSuccess = atomicStampedRef.compareAndSet(1,2,stamp,stamp +1);  //此时expectedReference未发生改变,但是stamp已经被修改了,所以CAS失败
        System.out.println("操作线程" + Thread.currentThread() +",CAS操作结果: " + isCASSuccess);
    },"主操作线程");

    Thread other = new Thread(() -> {
        Thread.yield(); // 确保thread-main 优先执行
atomicStampedRef.compareAndSet(1,2,atomicStampedRef.getStamp(),atomicStampedRef.getStamp() +1);
        System.out.println("操作线程" + Thread.currentThread() +",【increment】 ,值 = "+ atomicStampedRef.getReference());
        atomicStampedRef.compareAndSet(2,1,atomicStampedRef.getStamp(),atomicStampedRef.getStamp() +1);
        System.out.println("操作线程" + Thread.currentThread() +",【decrement】 ,值 = "+ atomicStampedRef.getReference());
    },"干扰线程");

    main.start();
    other.start();
}


输出结果:

> 操作线程Thread[主操作线程,5,main],初始值 a = 2
> 操作线程Thread[干扰线程,5,main],【increment】 ,值 = 2
> 操作线程Thread[干扰线程,5,main],【decrement】 ,值 = 1
> 操作线程Thread[主操作线程,5,main],CAS操作结果: false

四、总结

(1)在多线程环境下使用无锁结构要注意ABA问题;

(2)ABA的解决一般使用版本号来控制,并保证数据结构使用元素值来传递,且每次添加元素都新建节点承载元素值;

(3)AtomicStampedReference内部使用Pair来存储元素值及其版本号;

AtomicMarkableReference也可以解决ABA问题,它不是维护一个版本号,而是维护一个boolean类型的标记,标记值有修改,了解一下。

【参考文献】

Java并发编程-无锁CAS与Unsafe类及其并发包Atomic