CAS及ABA问题

╄→гoц情女王★ 提交于 2020-01-13 05:25:04

CAS

并发中线程安全必须保证三个要素,原子性、可见性、有序性。使用volatile可以保证可见性和有序性,但是不能保证原子性。所以还是会出现并发修改紊乱的问题。

这里的解决方法可以通过synchronized修饰,但是太重了,所以使用原子类保证原子性即可,但是原子类底层是如何保证原子性的值得研究,首先一点就是CAS。

CAS是什么

campareAndSwap.

JDK8,在AtomicInteger类中有一个getAndAdd()方法,点进去,可以看到:

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

    // setup to use Unsafe.compareAndSwapInt for updates
    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;
    /**
     * Atomically adds the given value to the current value.
     *
     * @param delta the value to add
     * @return the previous value
     */
    public final int getAndAdd(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }
    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;
    }
    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

可以看到,value为我们传过去的int值,而valueOffset是unsafe类(java RT包下的一个底层类)直接调用native方法进行计算得到的,是该value在当前对象中的的内存偏移量,标记了value的内存地址。

CAS的全称是compare and swap,它是一条CPU并发原语,由硬件提供支持保证线程互斥,从而保证原子性。**它的作用是判断内存某个位置的值是不是预期值,如果是,则更新这个值,这个过程是原子的。**但是它不能保证可见性,所以需要操作的值需要用volatile修饰。

主要看getAndAddInt()方法,这个原子类中的int value不仅仅是通过volatile修饰的,在赋值时使用了一个自旋锁进行期望值判断,如果是这个期望值与当前值匹配才进行更改。

首先使用当前对象(该原子类)和该值在当前对象的内存地址偏移量得到将要改变的当前值,得到当前值后,马上要准备修改了,但是可能还有别的线程在并发修改这个值,所以在执行修改操作之前再进行判断一次,是不是和期望值(刚才得到的当前值一样)如果一样,修改成功,返回true跳出循环,如果不一样,进入循环更新当前值(期望值),如此循环往复。

使用CAS而不使用synchronize的原因是,后者直接进行加锁,保证同一时间只有一个线程执行,保证了线程安全,但是性能下降。CAS不加锁,同一时间有多个线程进行访问,通过自旋判断预期值是否进行修改,性能较好,解决了部分线程安全的问题,但是不能保证ABA问题。

CAS缺点

  1. do while毕竟是个循环,如果一直有别的线程修改这个值导致当前线程更新失败则一直循环,会给CPU造成一定的负担
  2. 只能保证一个变量的原子操作
  3. 引申出ABA问题

ABA问题

在CAS自旋的过程中,判断是否循环条件是根据是否修改成功来的,是否修改成功是拿当前值和期望值对比进行判断。那么问题来了,在多线程并发的情况下,在与期望值对比时,比如期望值是8,其他线程已经将8 -> 9 ->10 -> 8又修改为8了,这样当前值相当于不变,当前线程修改成功,跳出自旋。

貌似保证了表面上的数值的正确性,但很明显CAS自旋的过程中已经被改变了很多次。这就是ABA问题,ABA问题是否需要解决得看具体的业务需求,业务需求只需保证数值正确性则没必要解决。业务需求既要保证数值的正确性也要保证当前线程在修改之前不允许其他线程修改,则需要解决ABA问题。

原子引用

@Data
@AllArgsConstructor
class User{
    private String username;
    private int age;
}

public class AtomicReferenceTest {
    @Test
    public void test() {
        User user1 = new User("lzj", 18);
        User user2 = new User("zzl", 19);

        AtomicReference<User> atomicReference = new AtomicReference<>();
        atomicReference.set(user1);
        atomicReference.compareAndSet(user1, user2);

        User user = atomicReference.get();
        System.out.println(user);
    }
}
public static void main(String[] args) {
    AtomicReference<Integer> atomicReference = new AtomicReference<>(100);
    new Thread(() -> {
        atomicReference.compareAndSet(100, 101);
        atomicReference.compareAndSet(101, 100);
    }).start();

    new Thread(() -> {
        try {
            Thread.sleep(100);
            System.out.println(atomicReference.compareAndSet(100, 2019) + "\t" + atomicReference.get());
            //很明显,这里修改成功了
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();
}

和原子类原理差不多,这里就不多说了。但是好像并不能解决ABA问题呀,这里继续引申出时间戳原子引用。相当于给原子类加上一个版本号,每次修改都会更新这个版本号,由原来的按期望值比较改成比较版本号,期望值修改后可能相同,但版本号修改后绝对不同,所以可以解决ABA问题。(类似于数据库中的乐观锁)

        AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<>(100, 1);
        new Thread(() -> {
            atomicStampedReference.compareAndSet(100, 101, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
        }).start();
        new Thread(() -> {
            atomicStampedReference.compareAndSet(101, 100, atomicStampedReference.getStamp(), atomicStampedReference.getStamp() + 1);
            System.out.println(atomicStampedReference.getReference());
        }).start();

时间戳原子类的compareAndSet()方法点击可以看到:

    /**
     * Atomically sets the value of both the reference and stamp
     * to the given update values if the
     * current reference is {@code ==} to the expected reference
     * and the current stamp is equal to the expected stamp.
     *
     * @param expectedReference the expected value of the reference
     * @param newReference the new value for the reference
     * @param expectedStamp the expected value of the stamp
     * @param newStamp the new value for the stamp
     * @return {@code true} if successful
     */
    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)));
    }

四个参数做比较,可以保证更新操作的原子性,分别是期望值、更新值、期望版本号、更新版本号。

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!