JUC原子类

2023年 9月 4日 98.6k 0

image.png

一、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类用于原子性地更新长整型类型。

案例:

public class AtomicIntegerTest {
    public static void main(String[] args) {
    
        AtomicInteger i = new AtomicInteger(0);
        
        //自增1并返回值,类似于++i,但是是原子性的。
        System.out.println(i.incrementAndGet());//1

        //自增1并返回值,类似于i++,但是是原子性的。
        System.out.println(i.getAndIncrement());//1

        //自减1并返回值,类似于++i,但是是原子性的。
        System.out.println(i.decrementAndGet());//1

        //自减1并返回值,类似于++i,但是是原子性的。
        System.out.println(i.getAndDecrement());//1

        //获取当前i的最新值
        System.out.println(i.get());//0

        //获取并自增指定的值。
        System.out.println(i.getAndAdd(-1));//0

        //自增并获取指定的值。
        System.out.println(i.addAndGet(-1));//-2

        //获取当前i的最新值
        System.out.println(i.get());//-2

        AtomicInteger j = new AtomicInteger(5);

        //先获取值后做计算,并且为原子运算。
        System.out.println(j.getAndUpdate(val -> val * 10));//5

        //先计算后重新设置i的值,并且为原子运算。
        System.out.println(j.updateAndGet(val -> val * 10));//500
        
        //判断j是不是等于j的最新值,如果相等,则将100赋值给j,如果有别的线程更改了j的值,那么该方法返回false。
        if (j.compareAndSet(j.get(),100)){
            System.out.println("j 最新值为:"+j.get());//j 最新值为:100
        }
    }
}

2. 原子更新数组

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

  • AtomicIntegerArray类用于原子性地更新整数类型的数组。
  • AtomicLongArray类用于原子性地更新长整型类型的数组。
  • AtomicReferenceArray类用于原子性地更新引用类型的数组。 其中常用的方法有:
  • int addAndGet(int i, int delta):将输入值和对应索引i位置的元素相加。
  • boolean compareAndSet(int i, int expect, int update):如果当前值等于预期值,则将数组索引为i位置的元素设置为update值。 举个AtomicIntegerArray例子:

案例1:

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);

案例2:

public static void main(String[] args) {
    //普通数组在多线程情况下的结果
    //[588, 592, 577, 576, 565, 561, 558, 574, 583, 574]
    demo(
            () -> new int[10],
            (array) -> array.length,
            (array, index) -> array[index]++,
            array -> System.out.println(Arrays.toString(array))
    );

    //JUC原子数组在多线程情况下的结果
    //[1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000, 1000]
    demo(
            () -> new AtomicIntegerArray(10),
            AtomicIntegerArray::length,
            AtomicIntegerArray::getAndIncrement,
            System.out::println
    );
}

    //Supplier -> 提供者 无中生有 ()-> 结果
    //Function -> 一个参数一个结果 (参数)-> 结果
    //BiFunction(参数一,参数二)-> 结果
    //Consumer (一个参数) -> void
    //BiConsumer 参数一,参数二)-> void
private static  void demo(Supplier arraySupplier, Function lengthFun, BiConsumer putConsumer, Consumer printConsumer) {
    ArrayList threadList = new ArrayList();
    T array = arraySupplier.get();
    int length = lengthFun.apply(array);
    for (int i = 0; i  {
                    for (int j = 0; j  {
        try {
            t.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
    //执行打印操作
    printConsumer.accept(array);
}

3. 原子更新引用类型

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

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

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

案例1:

public class Test {

    public static void main(String[] args) {

        // 创建两个Person对象,它们的id分别是101和102。
        Person p1 = new Person(1);
        Person p2 = new Person(2);

        // 新建AtomicReference对象,初始化它的值为p1对象
        AtomicReference ar = new AtomicReference(p1);

        // 讲p2的引用地址与值赋给p3
        ar.compareAndSet(p1, p2);
        Person p3 = (Person) ar.get();

        System.out.println(p3.toString());
       
        //`Person`并没有覆盖`equals()`方法,而是采用继承自`Object`的`equals()`方法;
        //`Object`中的`equals()`实际上是调用`==`去比较两个对象,即比较两个对象的地址是否相等。
        System.out.println(p3.equals(p1));//false
        //p3 与 p2 的引用地址值相同。
        System.out.println(p3.equals(p2));//true
    }
}

class Person {

    volatile long id;

    public Person(long id) {
        this.id = id;
    }

    public String toString() {
        return Long.toString(id);
    }
}

案例2:

public interface DecimalAccount {

    //获取余额
    BigDecimal getBalance();

    //取款
    void withdraw(BigDecimal amount);

    //方法中会启动1000个线程,每个线程-10元的操作,如果初始余额为10000,那么正确结果应该是0
    static void demo(DecimalAccount account) {
        ArrayList threadList = new ArrayList();
        //CAS在线程比较少的情况,最好少于CPU的核心数时,才能发挥出它最大的效率。
        for (int i = 0; i  {
                account.withdraw(BigDecimal.TEN);
            }
            ));
        }
        threadList.forEach(Thread::start);
        threadList.forEach(t -> {
            try {
                t.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        System.out.println(account.getBalance());
    }
}
public class DecimalAccountCas implements DecimalAccount {

    private AtomicReference balance;

    public DecimalAccountCas(BigDecimal balance) {
        this.balance = new AtomicReference(balance);
    }

    @Override
    public BigDecimal getBalance() {
        return balance.get();
    }

    @Override
    public void withdraw(BigDecimal amount) {
        while (true) {
            BigDecimal newest = balance.get();
            BigDecimal next = newest.subtract(amount);
            if (balance.compareAndSet(newest, next)) {
                break;
            }
        }
    }

    public static void main(String[] args) {
        DecimalAccount.demo(new DecimalAccountCas(new BigDecimal("10000")));
    }
}

4. 原子更新字段类

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

  • AtomicIntegerFieldUpdater:更新整型字段的更新器。
  • AtomicLongFieldUpdater:更新长整型字段的更新器。
  • AtomicStampedFieldUpdater: 原子更新带有版本号的引用类型。
  • AtomicStampedReference:更新带有版本号的引用类型,需要更新版本号和引用类型,主要为了解决ABA问题。
  • AtomicReferenceFieldUpdater这是一个基于反射的工具类,它能对指定类的指定的volatile引用字段进行原子更新。(注意这个字段不能是private的) 简单理解:就是对某个类中,被volatile修饰的字段进行原子更新。

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

案例1

public class Test {

    public static void main(String[] args) {
        Student student = new Student();
        //创建AtomicReferenceFieldUpdater更新器。
        //参数一: 需要更新的对象的class对象。
        //参数二: 需要更新的字段类型的class对象。
        //参数一: 需要更新的字段的属性名称。
        AtomicReferenceFieldUpdater updater = AtomicReferenceFieldUpdater.newUpdater(Student.class, String.class, "name");
        //参数一:需要修改的对象。
        //参数二:原始属性值是多少进行比较。
        //参数一:需要修改为什么。
        updater.compareAndSet(student,null,"zhangsan");
        System.out.println(student);
    }
}

class Student{

    //必须为volatile类型,否则报错:Must be volatile type
    volatile String name;

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + ''' +
                '}';
    }
}
public class TestAtomicIntegerFieldUpdater {

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

    public AtomicIntegerFieldUpdater 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));
        
        //报java.lang.IllegalArgumentException
        //System.out.println("staticVar = "+updater("staticVar").getAndIncrement(data));
        
        //下面报异常: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读取内存位置X的值为A
  • 线程1阻塞了;
  • 线程2读取内存位置X的值为A
  • 线程2修改内存位置X的值为B
  • 线程2修改又内存位置X的值为A
  • 线程1恢复,继续执行,比较发现还是A把内存位置X的值设置为C
  • 6.png 可以看到对线程1来说,第一次的A和第二次的A实际上并不是同一个A

    案例:

    public class ABATest {
    
        static AtomicReference ref = new AtomicReference("A");
    
        public static void main(String[] args) throws InterruptedException {
            String newest = ref.get();
            other();
            Thread.sleep(1000);
            System.out.println("将A->C:" + ref.compareAndSet(newest, "C"));
        }
    
        public static void other() {
    
            //t1线程将A->B
            new Thread(() -> {
                System.out.println("A->B:" + ref.compareAndSet(ref.get(), "B"));
            }, "t1").start();
    
            //t2线程将B->A
            new Thread(() -> {
                System.out.println("B->A:" + ref.compareAndSet(ref.get(), "A"));
            }, "t2").start();
        }
    }
    
    //执行结果
    A->B:true
    B->A:true
    将A->C:true
    

    利用AtomicStampedReference解决ABA问题

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

    AtomicStampReference类的属性

    //volatile修饰的pair
    private volatile Pair pair;
    

    AtomicStampReference类的构造方法

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

    AtomicStampReference的内部类Pair

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

    comparedAndSwap()方法

        //expectedReference :更新之前的原始值
        //newReference : 将要更新的新值
        // expectedStamp : 期待更新的标志版本
        // newStamp : 将要更新的标志版本
        public boolean compareAndSet(V expectedReference,V newReference,int expectedStamp,int newStamp) {
            // 获取当前的(元素值,版本号)对
            Pair 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 cmp, Pair val) {
            // 调用Unsafe的compareAndSwapObject()方法CAS更新pair的引用为新引用
            return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
        }
    
    • 如果元素值和版本号都没有变化,并且和新的也相同,返回true
    • 如果元素值和版本号都没有变化,并且和新的不完全相同,就构造一个新的Pair对象并执行CAS更新pair

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

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

    案例1:

    public class ABATest {
    
        static AtomicStampedReference ref = new AtomicStampedReference("A", 0);
    
        public static void main(String[] args) throws InterruptedException {
            //获取值
            String newest = ref.getReference();
            //获取版本号
            int stamp = ref.getStamp();
            other();
            Thread.sleep(1000);
            System.out.println("将A->C:" + ref.compareAndSet(newest, "C", stamp, stamp + 1));
        }
    
        public static void other() {
    
            //t1线程将A->B
            new Thread(() -> {
                //获取版本号
                int stamp = ref.getStamp();
                System.out.println("A->B:" + ref.compareAndSet(ref.getReference(), "B", stamp, stamp + 1));
            }, "t1").start();
    
            //t2线程将B->A
            new Thread(() -> {
                //获取版本号
                int stamp = ref.getStamp();
                System.out.println("B->A:" + ref.compareAndSet(ref.getReference(), "A", stamp, stamp + 1));
            }, "t2").start();
        }
    }
    //执行结果:
    A->B:true
    B->A:true
    将A->C:false
    

    案例2:

    private static AtomicStampedReference 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();
            }
            //此时expectedReference未发生改变,但是stamp已经被修改了,所以CAS失败
            boolean isCASSuccess = atomicStampedRef.compareAndSet(1,2,stamp,stamp +1);
            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
    

    AtomicMarkableReference

    AtomicStampedReference可以给原子引用加上版本号,追踪原子引用整个的变化过程,如:A->B->A->C,通过AtomicStampedReference可以知道引用变量中途被更改了几次。但是有时候,并不关心引用变量更改了几次,更加关心是否更改过,所以就有了AtomicMarkableReference

    案例:

    public class MyAtomicMarkableReference {
    
        static AtomicMarkableReference atomicStampedReference = new AtomicMarkableReference("tom",false);
     
        public static void main(String[] args) {
        
            boolean oldMarked = atomicStampedReference.isMarked();
            String oldReference = atomicStampedReference.getReference();
     
            System.out.println("初始化之后的标记:"+oldMarked);
            System.out.println("初始化之后的值:"+oldReference);
     
            String newReference = "jerry";
     
            boolean b =atomicStampedReference.compareAndSet(oldReference,newReference,true,false);
            if(!b){
                System.out.println("Mark不一致,无法修改Reference的值");
            }
            b =atomicStampedReference.compareAndSet(oldReference,newReference,false,true);
            if(b){
                System.out.println("Mark一致,修改reference的值为jerry");
            }
            System.out.println("修改成功之后的Mark:"+atomicStampedReference.isMarked());
            System.out.println("修改成功之后的值:"+atomicStampedReference.getReference());
        }
    }
    
    //执行结果:
    初始化之后的标记:false
    初始化之后的值:tom
    Mark不一致,无法修改Reference的值
    Mark一致,修改reference的值为jerry
    修改成功之后的Mark:true
    修改成功之后的值:jerry
    

    四、总结

  • 在多线程环境下使用无锁结构要注意ABA问题。
  • ABA的解决一般使用版本号来控制,并保证数据结构使用元素值来传递,且每次添加元素都新建节点承载元素值。
  • AtomicStampedReference内部使用Pair来存储元素值及其版本号。
  • 相关文章

    JavaScript2024新功能:Object.groupBy、正则表达式v标志
    PHP trim 函数对多字节字符的使用和限制
    新函数 json_validate() 、randomizer 类扩展…20 个PHP 8.3 新特性全面解析
    使用HTMX为WordPress增效:如何在不使用复杂框架的情况下增强平台功能
    为React 19做准备:WordPress 6.6用户指南
    如何删除WordPress中的所有评论

    发布评论