前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Juc并发编程10——原子类与ABA问题解决方案

Juc并发编程10——原子类与ABA问题解决方案

作者头像
用户10127530
发布2022-10-26 18:03:55
2750
发布2022-10-26 18:03:55
举报
文章被收录于专栏:半旧的技术栈半旧的技术栈

除了加锁以外,还可以使用原子类实现操作原子性。它底层采用CAS算法,使用简单、性能高效、线程安全。

简单示范下它的使用。

代码语言:javascript
复制
public class Demo24 {
    public static void main(String[] args) {
        AtomicInteger integer = new AtomicInteger(1);  // 不能向Integer一样自动装箱(Integer i = 1)
        System.out.println(integer.getAndIncrement()); //相当于i++
        System.out.println(integer.incrementAndGet()); //相当于++i
    }
}

如何验证它的原子性呢?看看下面的代码。

代码语言:javascript
复制
public class Demo25 {
    public static void main(String[] args) throws InterruptedException {
        AtomicInteger integer = new AtomicInteger();
        Runnable r = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000000; i++) {
                    integer.getAndIncrement();
                }
                System.out.println("thread finish...");
            }
        };
        new Thread(r).start();
        new Thread(r).start();
        TimeUnit.SECONDS.sleep(1);
        System.out.println(integer.get());
    }
}

运行输出的结果如下。确实是原子性的哦。

代码语言:javascript
复制
thread finish...
thread finish...
2000000

为什么会这么神奇呢?我们来阅读下源码一探究竟吧。

代码语言:javascript
复制
  private volatile int value;

  public AtomicInteger(int initialValue) {
        value = initialValue;
   }

   public AtomicInteger() {
   }

它使用volatile关键字修饰了value,这样在CAS操作时就不会出错。

代码语言:javascript
复制
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }

在静态块中,通过计算偏移地址,获取value相对于对象的偏移地址,这样就可以直接在对应内存对数据进行操作。

接着看看自增操作是如何完成的。

代码语言:javascript
复制
  public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }

Unsafe中看看。下面的参数列表中var1是数组或者对象(要修改的值是数组的元素或者对象的属性),var2offset偏移地址,var4delta,参数变化量。可以看到,它是一个do-while循环,如果CAS失败会重新尝试,一直到成功。

代码语言:javascript
复制
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2); // 以Volatile形式读取变量的值
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); //对变量进行CAS操作

        return var5;
    }

为了帮助大家更加深入理解,我举一个例子。比如线程1,2同时对变量A进行修改,线程1速度较快,读到它的值是1,准备CAS操作,线程2此时也开始读到数据是1,但是进行CAS操作时线程1在执行,因此它无法CAS成功。当线程1执行完CAS操作后,数据A的值变成2。此时线程2尝试下一轮获取变量的值就是2(变量通过volitile修饰了),然后进行CAS操作值就变成了3.不过由于是do-while循环,var5的值仍然是2,getAndAddInt返回的参数是2,因此也符合getAndIncrement的逻辑定义(先获取值再自增)。

用图片描述下。

在这里插入图片描述
在这里插入图片描述

如果是incrementAndGet的话,其底层逻辑会不会是while循环呢?答案是否,大家看看源码。

代码语言:javascript
复制
public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

原来是在原来的return的值基础上+1,这样就可以复用一个底层方法getAndAddInt实现两个不同的逻辑。这个思想值得大家学习呀。

可见,原子类底层也是采用CAS算法保证的操作原子性,并且它提供了compareAndSet直接给外部使用。

代码语言:javascript
复制
		// 第一个值是期望值,第二个值是要更新后数据,符合期望值expect就会更新数据为update
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }

写个demo测试下。

代码语言:javascript
复制
public class Demo26 {
    public static void main(String[] args) {
        AtomicInteger integer = new AtomicInteger(10);
        integer.compareAndSet(20, 15); // fail
        System.out.println(integer);
        integer.compareAndSet(10, 30); // success
        System.out.println(integer);
    }
}

它还提供了lasySet方法,让你以普通变量而非volitile来操作数据,请读者自行了解。

除了基本数据类型有对应的原子类以外,基本的数组类型也有原子类。

在这里插入图片描述
在这里插入图片描述

演示如下。

代码语言:javascript
复制
public class Demo27 {
    public static void main(String[] args) throws InterruptedException {
        AtomicIntegerArray array = new AtomicIntegerArray(new int[]{0,3,5,9});
        Runnable r = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000000; i++) {
                    array.getAndAdd(0,1);
                }
                System.out.println("thread finish...");
            }
        };
        new Thread(r).start();
        new Thread(r).start();
        TimeUnit.SECONDS.sleep(1);
        System.out.println(array.get(0));
    }
}

在jdk1.8以后,juc新增了LongAdderDoubleAdder,在高并发场景下,它的性能比AtomicLong,AtomicDouble会更好。

它的原理简单介绍如下。如下图,如果是多个线程对atomicLong进行操作,每次只能有一个线程成功CAS,而其它线程都会循环进行CAS直到成功。这样线程等待时间会随着等待队列变长而增加,时间性能不佳。但是LongAdder会自己维护一个cell[]数组,不同的线程都可以操作数组中的不同元素进行CAS,最后再进行求和累加,一次性更新value.

在这里插入图片描述
在这里插入图片描述

使用实例如下。

代码语言:javascript
复制
public class Demo28 {
    public static void main(String[] args) throws InterruptedException {
        LongAdder integer = new LongAdder();
        Runnable r = new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 1000000; i++) {
                    integer.add(1);
                }
            }
        };
        for (int i = 0; i < 100; i++) {
            new Thread(r).start();
        }
        TimeUnit.SECONDS.sleep(1);
        System.out.println(integer.sum());
    }
}

除了对于基本数据类型有原子操作的支持外,对于引用类型,也可以实现原子操作。

代码语言:javascript
复制
public class Demo29 {
    public static void main(String[] args) {
        String a = "hello a";
        String b = "hello b";
        AtomicReference<String> reference = new AtomicReference<>(a);
        reference.compareAndSet(a, b);
        System.out.println(reference.get());
    }
}

juc还提供了字段原子更新器,我们可以对于类中的某个字段进行原子的更新操作(注意字段必须使用vilotile关键字修饰)。

代码语言:javascript
复制
public class Demo30 {
    public static void main(String[] args) {
        Student student = new Student();
        AtomicIntegerFieldUpdater<Student> updater = AtomicIntegerFieldUpdater.newUpdater(Student.class, "age");
        System.out.println(updater.incrementAndGet(student));
    }

    public static class Student{
        volatile int age;
    }
}

到目前为止,有关原子类的相关介绍结束了。现在我们想象下面的场景。

在这里插入图片描述
在这里插入图片描述

线程1,2同时CAS修改变量a的值,线程1速度较快,将a修改为2以后又把a修改为了1,这时线程2才开始判断,发现a的值就是expect的期望值1,于是CAS成功,将变量a修改为了2.很明显,这个时候的1与初始的1不是同一个1了,对于基本数据类型可能还不算太坏,但是对于string等这可不妙,这其实是CAS操作的一个问题,它只会机械的比较当前值是否与期望值一致,并不能知道当前值是否被修改过。这种问题就被称为ABA问题。

如何解决ABA问题呢,juc提供了带版本号的CAS操作,只要每次操作记录下版本号,并且版本号不重复就可以了。

代码语言:javascript
复制
public class Demo31 {
    public static void main(String[] args) {
        String a = "hello a";
        String b = "hello a";
        AtomicStampedReference<String> reference = new AtomicStampedReference<>(a, 1);
        reference.attemptStamp(a,2);
        System.out.println(reference.compareAndSet(a, b, 2, 3));
        
    }
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-04-28,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档