原子操作类的使用以及ABA问题的解决

原子操作类包括以下几类:

  • 基本类:AtomicInteger、AtomicLong、AtomicBoolean。

  • 引用类型:AtomicReference、AtomicStampedRerence、AtomicMarkableReference。

  • 数组类型:AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray。

  • 属性原子修改器(Updater):AtomicIntegerFieldUpdater、AtomicLongFieldUpdater、AtomicReferenceFieldUpdater。

原子操作类的使用

AtomicInteger的使用

AtomicInteger的常用方法如下:

  • int addAndGet(int delta):以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。

  • boolean compareAndSet(int expect,int update):如果输入的数值等于预期值,则以原子方式将该值设置为输入的值。

  • int getAndIncrement():以原子方式将当前值加1,注意,这里返回的是自增前的值。

  • int getAndSet(int newValue):以原子方式设置为newValue的值,并返回旧值。

  • int incrementAndGet():以原子方式将当前值加1后返回。

package com.morris.concurrent.thread.atomic;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;

public class AtomicIntegerDemo {

    public static AtomicInteger inc = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {

        CountDownLatch countDownLatch = new CountDownLatch(10);

        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    inc.incrementAndGet();
                    ///System.out.println(inc.get());
                }
                countDownLatch.countDown();
            }).start();
        }

        countDownLatch.await(); // 保证前面的线程都执行完
        System.out.println(inc.get()); // 10000

    }
}

AtomicReference的使用

package com.morris.concurrent.atomic.primary;

import java.util.concurrent.atomic.AtomicReference;

public class AtomicReferenceDemo {
    public static void main(String[] args) {
        Person p1 = new Person("bob");
        Person p2= new Person("morris");
        AtomicReference<Person> atomicReference = new AtomicReference(p1);
        atomicReference.compareAndSet(p1, p2);
        System.out.println(atomicReference.get().name);
 // morris
    }

    private static class Person {
        String name;
        Person(String name) {
            this.name = name;
        }
    }
}

AtomicIntegerArray的使用

package com.morris.concurrent.atomic;

import java.util.concurrent.atomic.AtomicIntegerArray;

public class AtomicIntegerArrayDemo {
    public static void main(String[] args) {
        AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(10);
        atomicIntegerArray.set(1, 10);
        atomicIntegerArray.compareAndSet(1, 10, 20);
        System.out.println(atomicIntegerArray.get(1)); // 20
    }
}

AtomicIntegerFieldUpdater的使用

package com.morris.concurrent.atomic;

import java.util.concurrent.atomic.AtomicIntegerFieldUpdater;


public class AtomicIntegerFieldUpdaterDemo {
    public static void main(String[] args) {
        Person person = new Person();
        AtomicIntegerFieldUpdater atomicIntegerFieldUpdater = AtomicIntegerFieldUpdater.newUpdater(Person.class, "age");
        atomicIntegerFieldUpdater.set(person, 18);
        atomicIntegerFieldUpdater.compareAndSet(person, 18, 20);
        System.out.println(atomicIntegerFieldUpdater.get(person));
 // 20
    }

    private static class Person {
        volatile int age; // 注意age必须用volatile修饰
    }
}

ABA问题

ABA问题的发生

ABA:假设有另外一个线程将值原来是A,先修改成B,再修改回成A,当前线程的CAS操作无法分辨当前V值是否发生过变化。

package com.morris.concurrent.thread.atomic;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

public class ABADemo {

    private static AtomicInteger atomicInteger = new AtomicInteger(0);

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                int expect = 0;
                int update = 1;
                if (atomicInteger.compareAndSet(expect, update)) {
                    System.out.printf("%s将值[%d]修改为[%d]\n", Thread.currentThread().getName(), expect, update);
                }
            }).start();
        }

        new Thread(() -> {
            int expect = 1;
            int update = 0;
            while (!atomicInteger.compareAndSet(expect, update)) {
            }
            System.out.printf("%s将值[%d]修改为[%d]\n", Thread.currentThread().getName(), expect, update);
        }).start();
    }

}

运行结果如下:

Thread-0将值[0]修改为[1]
Thread-10将值[1]修改为[0]
Thread-3将值[0]修改为[1]

可以发现,有两个线程修改了这个值,我们是想那一堆将0改成1的线程仅有一个成功。此时我们通过类来AtomicStampedReference或AtomicMarkableReference解决这个问题。

使用AtomicStampedReference解决ABA问题

AtomicStampedReference使用了版本号来解决ABA问题,值的每次修改都会更新版本号,每次比较时不仅会比较值,还会比较版本号。

package com.morris.concurrent.atomic;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicStampedReference;

public class ABAAtomicStampedReference {
    // 第一个参数为初始值0
    // 第二个参数为初始版本为0
    private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference(0, 0);

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                int expect = 0;
                int update = 1;
                int stamp = 0;
                int newStamp = 1;
                if (atomicStampedReference.compareAndSet(expect, update, stamp, newStamp)) {
                    System.out.printf("%s将值[%d]修改为[%d],版本[%d]修改为[%d]\n", Thread.currentThread().getName(), expect, update, stamp, newStamp);
                }
            }).start();
        }

        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            int expect = 1;
            int update = 0;
            int stamp = 1;
            int newStamp = 2;
            if (atomicStampedReference.compareAndSet(expect, update, stamp, newStamp)) {
                System.out.printf("%s将值[%d]修改为[%d],版本[%d]修改为[%d]\n", Thread.currentThread().getName(), expect, update, stamp, newStamp);
            }
        }).start();
    }
}

使用AtomicMarkableReference解决ABA问题

AtomicMarkableReference使用标记来解决ABA问题,值的每次修改都会更新标记,每次比较时不仅会比较值,还会比较标记。

AtomicStampedReference关心的是值被改了多少次。而AtomicMarkableReference关心的值有没有被改变。

package com.morris.concurrent.atomic;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicMarkableReference;


public class ABAAtomicMarkableReference {
    // 第一个参数为初始值0
    // 第二个参数为初始标记为0
    private static AtomicMarkableReference<Integer> atomicStampedReference = new AtomicMarkableReference(0, true);

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                int expect = 0;
                int update = 1;
                boolean expectMark = true;
                boolean newMark = false;
                if (atomicStampedReference.compareAndSet(expect, update, expectMark, newMark)) {
                    System.out.printf("%s将值[%d]修改为[%d],标记[%s]修改为[%s]\n", Thread.currentThread().getName(), expect, update, expectMark, newMark);
                }
            }).start();
        }

        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            int expect = 1;
            int update = 0;
            boolean expectMark = false;
            boolean newMark = true;
            if (atomicStampedReference.compareAndSet(expect, update, expectMark, newMark)) {
                System.out.printf("%s将值[%d]修改为[%d],标记[%s]修改为[%s]\n", Thread.currentThread().getName(), expect, update, expectMark, newMark);
            }
        }).start();
    }
}

实现原理

Atomic包里的类基本都是使用Unsafe实现的,让我们一起看一下Unsafe的源码。

public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x);
public final native boolean compareAndSwapInt(Object o, long offset, int expected, int x);
public final native boolean compareAndSwapLong(Object o, long offset, long expected, long x);

参数解释:

  • 参数1:对象所在的类本身的对象(一般这里是对一个对象的属性做修改,才会出现并发,所以该对象所存在的类也是有一个对象的)。

  • 参数2:这个属性在这个对象里面的相对偏移量位置,其实对比时是对比内存单元,所以需要属性的起始位置,而引用就是修改引用地址(根据OS、VM位数和参数配置决定宽度一般是4-8个字节),int就是修改相关的4个字节,而long就是修改相关的8个字节。获取偏移量也是通过unsafe的一个方法:objectFieldOffset(Fieldfield)来获取属性在对象中的偏移量;静态变量需要通过staticFieldOffset(Field field)获取,调用的总方法是:fieldOffset(Fieldfield)。

  • 参数3:修改的引用的原始值,用于对比原来的引用和要修改的目标是否一致。

  • 参数4:修改的目标值,要将数据修改成什么。

原子操作类的使用以及ABA问题的解决

对象的引用进行对比后交换,交换成功返回true,交换失败返回false,这个交换过程完全是原子的,在CPU上计算完结果后,都会对比内存的结果是否还是原先的值,若不是,则认为不能替换,因为变量是volatile类型所以最终写入的数据会被其他线程看到,所以一个线程修改成功后,其他线程就发现自己修改失败了。

Atomic包提供了3种基本类型的原子更新,但是Java的基本类型里还有char、float和double等。那么如何原子的更新其他的基本类型呢?

通过代码,我们发现Unsafe只提供了3种CAS方法:compareAndSwapObject、compareAndSwapInt和compareAndSwapLong,再看AtomicBoolean源码,发现它是先把Boolean转换成整型,再使用compareAndSwapInt进行CAS,所以原子更新char、float和double变量也可以用类似的思路来实现。

上一篇:2019年互联网面试题第二季(1.3)


下一篇:Java多线程(三)显式锁和AQS