Atomic并发原子类源码及使用示例 解决ABA问题

Atomic原子操作类是JUC包下的类,包含AtomicInteger、AtomicBoolean、AtomicLong、AtomicReference等等,AtomicReference是可以自定定义类型的原子操作类。原理都是使用volatile和CAS(比较并交换)完成操作。

volatile是java关键字,轻量级的同步机制,用来保证变量的可见性,用于多线程。线程的操作是在线程私有的工作内存中,而变量的存储是在主内存中,所以线程修改变量时先把变量copy到工作内存中,操作完成后回写到主内存。volatile保证了变量可见性,当一个线程修改变量后,会将结果写到主内存中,并通知其他线程变量已被修改,需要重新从主内存中获取。

class MyCount {
    int n;
    AtomicInteger ai = new AtomicInteger(0);
}

public class AtomicTest {
    public static void main(String[] args) {
        MyCount myCount = new MyCount();
        for (int i = 0; i < 30; i++) {
            new Thread(() -> {
                for (int j = 0; j < 500; j++) {
                    myCount.n++;
                    myCount.ai.getAndIncrement(); // 对应n++
                }
            }).start();
        }
        // 等待线程执行完成
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("n=" + myCount.n);
        System.out.println("ai=" + myCount.ai);
    }
}

运行结果
n=14869
ai=15000

由于n++不是原子操作(要么都执行,要么都不执行),分为3步:1 获取n值;2 值+1;3 结果赋值给n。如果两个线程同时执行,线程1取到n为1,并且准备将2赋值给n时,线程1被挂起,此时线程2获取n也为1,执行完成后将n改为2,线程1继续执行,将线程2的结果覆盖。导致最后执行结果不是15000。

而AtomicInteger.getAndIncrement()属于原子操作,运行结果为15000。我们来看一下AtomicInteger类的源码:

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    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); }
    }
    
    private volatile int value;
    
    public AtomicInteger(int initialValue) {
        value = initialValue;
    }
    
    public AtomicInteger() {
    }
    ...
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    ...
}

AtomicInteger提供了很多操作方法,本质都是调用了Unsafe类的方法,追踪一下Unsafe类:

public final class Unsafe {

    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
    
	public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }
}

Unsafe在执行getAndAddInt方法时,使用了do while循环,相当于自旋。首先从主内存中获取最新的值,然后通过CAS比较刚获取的结果,如果一致则改为新值, 如果不一致,重新从内存中取并再次比较交换,直至成功。

Unsafe类是sun.misc包下的类,存在于JDK中lib目录下的rt.jar包中,内部封装了很多CAS方法,属于本地方法,是通过汇编语言操作CPU指令,防止执行被加塞的原子操作。

CAS类似于MySQL数据操作中的乐观锁,更新时比较旧值。CAS有以下缺点:

1. 空循环,do while失败一致循环操作
2. 只能对一个变量进行CAS
3. ABA问题(重点)

ABA问题:当操作数同时被多个线程做加和减时,线程1取到值后,改为2,线程2取到后改回了1,线程3取到1为1,然后改为2,线程3对线程1和线程2的操作无感知。解决此问题可以使用AtomicStampedReference类进行操作。

AtomicStampedReference原理就是用两个CAS进行判定操作,一个对变量操作, 另一个是stamp,应该每次操作固定加或者减,如果变量被ABA操作,但是stamp值已经发生改变,CAS会返回失败。

class MyCount {
    AtomicInteger ai = new AtomicInteger(0);
    AtomicStampedReference<Integer> asr = new AtomicStampedReference<>(0, 0);
}

public class AtomicTest {
    public static void main(String[] args) {
        MyCount myCount = new MyCount();
        int stamp = myCount.asr.getStamp();
        new Thread(() -> {
            myCount.ai.compareAndSet(0, 1);
            myCount.ai.compareAndSet(1, 0);
            System.out.println(Thread.currentThread().getName() + "执行后 ai=" + myCount.ai);

            myCount.asr.compareAndSet(0, 1, stamp, stamp + 1);
            myCount.asr.compareAndSet(1, 0, myCount.asr.getStamp(), myCount.asr.getStamp() + 1);
            System.out.println(Thread.currentThread().getName() + "执行后 asr=" + myCount.asr.getReference());
        }, "Thread1").start();
        new Thread(() -> {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myCount.ai.compareAndSet(0, 1);
            System.out.println(Thread.currentThread().getName() + "执行后 ai=" + myCount.ai);

            boolean result = myCount.asr.compareAndSet(0, 1, stamp, stamp + 1);
            System.out.println(Thread.currentThread().getName() + "执行" + result + " asr=" + myCount.asr.getReference());
        }, "Thread2").start();
    }
}

运行结果
Thread1执行后 ai=0
Thread1执行后 asr=0
Thread2执行后 ai=1
Thread2执行false asr=0

在CAS方法内,判断原reference、stamp是否和当前值一致, 如果不一致返回false;如果一致,判断新的reference、stamp未发生改变,返回成功, 如果发生了改变,使用Unsafe的本地方法调用CAS交换对象,返回结果

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

    private volatile Pair<V> pair;
    ...
	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) ||
             casPair(current, Pair.of(newReference, newStamp)));
    }
    ...
    private boolean casPair(Pair<V> cmp, Pair<V> val) {
        return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
    }
    ...
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值