Java并发系列(5)——CAS与Java原子操作类

接上一篇《Java并发系列(4)——ThreadLocal实现原理与内存泄漏分析

4 CAS

4.1 什么是 CAS

CAS,即 Compare And Swap,是 cpu 在硬件层面上支持的一个指令。

CAS 指令接收三个参数:

  • 变量;
  • 期望值;
  • 待写入值。

CAS 指令的逻辑可以用以下代码表示(以 int 变量为例):

public boolean compareAndSwap(int i, int expect, int update) {
    if (i == expect) {
        i = update;
        return true;//变量当前值与期望值相同,更新成功
    }
    reture false;//否则,更新失败
}

当然,这整个操作是原子性的,这一点由 cpu 保证。

4.2 CAS 的作用

借助于 CAS 指令,可以对锁进行一些优化。

为了解决线程共享变量访问的线程安全问题,以往常常使用加锁的方式,比如 synchronized:

public synchronized int increment() {
    i++;//访问线程共享变量
}

这种方式非常安全,但问题是,加锁是一个很重的操作,涉及线程上下文切换,冲刷处理器缓存等。

而借助于 CAS,则可以这样来解决:

    public void increment() {
        int oldValue, newValue;
        boolean success;
        do {
            oldValue = i;
            newValue = i + 1;
            success = compareAndSwap(i, oldValue, newValue);
            if (!success) {
                System.out.println(success);
            }
        } while (!success);
    }
  • 先读取更新之前的 oldValue;
  • 经过运算得到 newValue;
  • 用 CAS 指定更新:
    • 如果发现此时变量值与 oldValue 不同,则说明在此期间变量值被其它线程更新过了,更新失败,从头再来;
    • 如果此时变量值与 oldValue 相同,则说明没有被其它线程更新过,就可以将 newValue 写入。

4.3 问题与解决办法

4.3.1 CAS 只保证原子性,不保证可见性

可以用下面代码来证明这一点:

package per.lvjc.concurrent.cas;

import sun.misc.Unsafe;

public class CASVisibilityTest {

    private static int value;

    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

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

    private static void atomicIncrement() {
        int oldValue, newValue;
        boolean success;
        do {
            oldValue = value;
            newValue = oldValue + 1;
            success = unsafe.compareAndSwapInt(CASVisibilityTest.class, valueOffset, oldValue, newValue);
            if (!success) {
                System.out.println(success);
            }
        } while (!success);
    }

    public static void main(String[] args) throws InterruptedException {
        Thread read = new Thread(() -> {
            System.out.println("read begin");
            while (value < 5) {
                if (value != 0) {
                    System.out.println("read: value = " + value);
                }
            }
            System.out.println("read end");
        });
        Thread write = new Thread(() -> {
            System.out.println("write begin");
            while (value < 5) {
                atomicIncrement();
                System.out.println("write: value = " + value);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("write end");
        });
        read.start();
        Thread.sleep(100);
        write.start();
    }
}

这里用到了 Unsafe 类,后面会提到,这里主要是通过它来调用 compareAndSwapInt 方法。

让 read 线程先跑,它会把共享变量 value 的值读进去;write 线程后跑,通过 CAS 方式更新 value 变量的值。

执行结果如下:

read begin
write begin
write: value = 1
write: value = 2
write: value = 3
write: value = 4
write: value = 5
write end

read 线程没有感知到 write 线程对变量 value 的更新,因此陷入了死循环,程序一直没有结束,“read end” 一直没有打印出来。

解决办法:配合 volatile 使用,volatile 恰好保证可见性而不保证原子性。

4.3.2 ABA 问题

前面说,“如果此时变量值与 oldValue 相同,则说明没有被其它线程更新过”。这一点其实是不一定的。

因为存在一种场景:

  • 我读取变量值为 A;
  • 其它线程将变量值更改为 B;
  • 其它线程又将变量值更改为 A;
  • 这时我去更新,发现此时变量值与我之前读到的值是相同的,就以为没有被其它线程更新过。

对于只关心结果,而不关心中间过程的场景,那么 ABA 问题可以忽略。

对于需要关心中间过程的场景,解决办法是:加入版本号。

  • 我读取变量值为 A,版本号为 1;
  • 其它线程将变量值更改为 B,同时提升版本号为 2;
  • 其它线程又将变量值更改为 A,同时提升版本号为 3;
  • 这是我去更新,发现变量值虽然相同,但版本号不同,说明其它线程有过更新。
4.3.3 对于耗时操作的开销问题

由于 CAS 更新失败,会从头开始,对于 i++ 这种简单的操作,从头开始哪怕循环几千次问题也不大。

但如果是耗时操作,那么重新执行一遍代价就太大了,因为它会一直占用 cpu 及其它运算资源。

反而不如锁操作,干脆让线程暂时挂起,让出资源。

4.3.4 CAS 只能操作一个变量

CAS 操作只支持当个变量,如果要保证多个变量 CAS 操作的原子性,办法是:将多个变量封装成一个对象,对对象引用做 CAS 操作。

4.4 Unsafe 类

Unsafe 类是一个底层工具类,被 JDK 里面其它类大量用到,包括下面将要介绍的 JDK 提供的一些原子操作类。

这个类就像它的名字一样,是非常不安全的,因此不推荐直接使用。事实上也没办法通过一般手段拿到 Unsafe 实例。

4.4.1 获取 Unsafe 实例
    private static final Unsafe theUnsafe;

    private Unsafe() {
    }

    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class var0 = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
            throw new SecurityException("Unsafe");
        } else {
            return theUnsafe;
        }
    }

构造函数是 private,而仅有的一个静态获取方法还要求调用者必须是由启动类加载器加载的类。

所以有两种方法可以拿到 Unsafe 实例:

  • 反射;
  • 通过 -Xbootclasspath 参数将需要调用 getUnsafe 方法的类添加到启动类加载器加载列表。
4.4.2 调用 Unsafe 方法

Unsafe 类的方法都是本地方法,可以做一些非常底层的操作。

比如,操作某个实例在某个偏移量处的变量:

    public native int getInt(Object var1, long var2);

    public native void putInt(Object var1, long var2, int var4);

    public native Object getObject(Object var1, long var2);

    public native void putObject(Object var1, long var2, Object var4);

	...

比如,直接对某个内存地址的变量进行操作:

    public native byte getByte(long var1);

    public native void putByte(long var1, byte var3);

    public native short getShort(long var1);

    public native void putShort(long var1, short var3);

	...

比如,操作内存:

    public native long allocateMemory(long var1);

    public native long reallocateMemory(long var1, long var3);

    public native void setMemory(Object var1, long var2, long var4, byte var6);

    public void setMemory(long var1, long var3, byte var5) {
        this.setMemory((Object)null, var1, var3, var5);
    }

    public native void copyMemory(Object var1, long var2, Object var4, long var5, long var7);

    public void copyMemory(long var1, long var3, long var5) {
        this.copyMemory((Object)null, var1, (Object)null, var3, var5);
    }

    public native void freeMemory(long var1);

当然,compareAndSwap 方法也在这个类里面:

    public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);

    public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

    public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

其它方法就不再介绍了。

4.5 JDK 原子操作类

Java 提供常用的原子操作类有:

  • 基本类型:AtomicBoolean,AtomicInteger,AtomicLong;
  • 引用类型:AtomicReference,AtomicMarkableReference,AtomicStampedReference;
  • 数组类型:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray;
  • 字段类型:AtomicReferenceFieldUpdater,AtomicIntegerFieldUpdater,AtomicLongFieldUpdater。
4.5.1 基本类型原子类

以 AtomicInteger 类为例。

底层是借助于 Unsafe 类实现的:

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;
    
    //...
}

它里面存储了一个 int 类型的变量 value,以及 value 变量在 AtomicInteger 类中的内存偏移量,如果自己想使用 Unsafe 类可以参考。

同时,注意这里的 value 申明了 volatile,因为 CAS 并不保证可见性。

4.5.1.1 使用示例
public class AtomicIntegerTest {

    private static final AtomicInteger sum = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = () -> {
            for (int i = 0; i < 10000; i++) {
                sum.getAndIncrement();
            }
        };
        int size = 10;
        Thread[] threads = new Thread[size];
        for (int i = 0; i < size; i++) {
            Thread thread = new Thread(runnable);
            thread.start();
            threads[i] = thread;
        }
        for (int i = 0; i < size; i++) {
            threads[i].join();
        }
        System.out.println("sum = " + sum);
    }
}

定义了 AtomicInteger 类型的线程共享变量 sum,使用 getAndIncrement() 做原子性的自增。

十个线程并发自增一万次,sum 的计算结果一定等于 10 万。

4.5.2 引用类型原子类

Java 提供的引用类型的原子类有 AtomicReference,AtomicMarkableReference,AtomicStampedReference 三个。

4.5.2.1 用途

关于引用类型原子类的用途,可以先看这样一个场景:

public class AtomicReferenceTest {

    private static String name = "";
    private static String money = "";

    public static void main(String[] args) throws InterruptedException {
        for (int i = 1; i <= 20; i++) {
            final int count = i;
            new Thread(() -> {
                String myName = "t" + count + ",";
                name += myName;
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                String myMoney = count * 10 + ",";
                money += myMoney;
            }).start();
        }
        Thread.sleep(1000);
        System.out.println("name:" + name);
        System.out.println("money:" + money);
        System.out.println("count:" + name.split(",").length);
    }
}

比如说现在我要结婚了,要记录哪些人给我发了红包,分别发了多少钱。发红包的人名记录在 name 字段,红包金额记录在 money 字段。现在有 20 个人(线程),依次将自己的名字和金额,写在前面一个人的后面。

这里 sleep(1) 是为了线程切换,更容易出现我们想看到的结果。

因为非原子性的问题,多跑几次,会看到类似这样的结果:

name:t2,t3,t5,t6,t7,t10,t9,t4,t11,t8,t12,t13,t15,t14,t16,t17,t19,t18,t20,
money:20,60,70,30,10,50,40,80,120,160,90,130,110,150,100,140,180,190,170,200,
count:19

可以看到有两个问题:人名跟金额没有对上(如 t3 对应的应该是 30 而不是 60),还记漏了一个人(应该有 20 个却只记了 19 个)。

第一次改进:

public class AtomicReferenceTest {

    private static class NameMoney {
        public NameMoney(String name, String money) {
            this.name = name;
            this.money = money;
        }
        private String name;
        private String money;
    }

    private static NameMoney nameMoney = new NameMoney("", "");

    public static void main(String[] args) throws InterruptedException {
        for (int i = 1; i <= 20; i++) {
            final int count = i;
            new Thread(() -> {
                final NameMoney localNameMoney = nameMoney;
                String myName = "t" + count + ",";
                String myMoney = count * 10 + ",";
                nameMoney = new NameMoney(localNameMoney.name + myName, localNameMoney.money + myMoney);
            }).start();
        }
        Thread.sleep(1000);
        System.out.println("name:" + nameMoney.name);
        System.out.println("money:" + nameMoney.money);
        System.out.println("count:" + nameMoney.name.split(",").length);
    }
}

把 name 和 money 两个变量封装到一个类里面,一次同时更新两个字段。

多跑几次,会看到结果:

name:t1,t3,t7,t6,t4,t5,t11,t8,t9,t2,t14,t13,t12,t16,t20,t15,t19,t17,t18,
money:10,30,70,60,40,50,110,80,90,20,140,130,120,160,200,150,190,170,180,
count:19

现在人名和金额对上了,但仍然会记漏掉,因为一个线程在写入时覆盖了其它线程的写入。

使用 AtomicReference:

public class AtomicReferenceTest {

    private static class NameMoney {
        public NameMoney(String name, String money) {
            this.name = name;
            this.money = money;
        }
        private String name;
        private String money;
    }

    private static NameMoney nameMoney = new NameMoney("", "");
    private static AtomicReference<NameMoney> atomicNameMoney = new AtomicReference<>(nameMoney);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 1; i <= 20; i++) {
            final int count = i;
            new Thread(() -> {
                boolean success = false;
                do {
                    final NameMoney localNameMoney = atomicNameMoney.get();
                    String myName = "t" + count + ",";
                    String myMoney = count * 10 + ",";
                    NameMoney newNameMoney = new NameMoney(localNameMoney.name + myName, localNameMoney.money + myMoney);
                    success = atomicNameMoney.compareAndSet(localNameMoney, newNameMoney);
                } while (!success);
            }).start();
        }
        Thread.sleep(1000);
        NameMoney nameMoney = atomicNameMoney.get();
        System.out.println("name:" + nameMoney.name);
        System.out.println("money:" + nameMoney.money);
        System.out.println("count:" + nameMoney.name.split(",").length);
    }
}

把原来的对象再封装一次:

AtomicReference<NameMoney> atomicNameMoney = new AtomicReference<>(nameMoney);

先把旧值读取出来,

final NameMoney localNameMoney = atomicNameMoney.get();

在旧值的基础上更新,然后用 CAS 写入:

atomicNameMoney.compareAndSet(localNameMoney, newNameMoney);

如果在写入时,已经有其它线程先更新过,导致我们读取了过期的数据,那么这里写入就会失败,然后进入循环重做“read-modify-write”操作,直到成功为止。

这样就不会有问题了。

4.5.2.2 三个原子类的区别

AtomicReference,AtomicMarkableReference,AtomicStampedReference 三者的区别是:

  • AtomicReference 不保证 ABA 问题;
  • AtomicMarkableReference 可以解决 ABA 问题,它会记录原数据是否被更新过;
  • AtomicStampedReference 也可以解决 ABA 问题,它不仅会记录原数据是否被更新过,还能记录被更新过多少次。

AtomicReference:

public class AtomicReferenceABA {

    private static class Foo {
        public Foo(int i) {
            this.i = i;
        }
        private int i = 0;
    }

    private static AtomicReference<Foo> atomicFoo = new AtomicReference<>(new Foo(1));

    public static void main(String[] args) {
        //线程 1 读取旧值
        Foo old = atomicFoo.get();
        //模拟线程 2 将 A 改为 B
        atomicFoo.set(new Foo(2));
        //old.i = 4;
        //模拟线程 1 的 CAS 更新
        boolean success = atomicFoo.compareAndSet(old, new Foo(5));
        System.out.println(success);
        //模拟线程 2 将 B 再改回 A
        atomicFoo.set(old);
        //模拟线程 1 的 CAS 更新
        success = atomicFoo.compareAndSet(old, new Foo(6));
        System.out.println(success);
    }
}

执行结果:

false
true

第一次更新后,compareAndSet 就失败了,但是再改回原值后,compareAndSet 就成功了。

另外,如果用代码中注释掉的old.i = 4来更新旧值,第一次 compareAndSet 会返回 true,说明 compareAndSet 在 compare 的时候是用 == 来判断的。

AtomicMarkableReference:

public class AtomicMarkableReferenceTest {

    private static class Foo {
        public Foo(int i) {
            this.i = i;
        }
        private int i = 0;
    }

    private static AtomicMarkableReference<Foo> atomicFoo = new AtomicMarkableReference<>(new Foo(1), false);

    public static void main(String[] args) {
        //线程 1 读取旧值
        boolean marked = atomicFoo.isMarked();
        Foo old = atomicFoo.getReference();
        //模拟线程 2 将 A 改为 B
        atomicFoo.set(new Foo(2), !marked);
        //模拟线程 1 的 CAS 更新
        boolean success = atomicFoo.compareAndSet(old, new Foo(5), marked, !marked);
        System.out.println(success);
        //模拟线程 2 将 B 再改回 A
        atomicFoo.set(old, !marked);
        //atomicFoo.set(old, marked);
        //模拟线程 1 的 CAS 更新
        success = atomicFoo.compareAndSet(old, new Foo(6), marked, !marked);
        System.out.println(success);
    }
}

执行结果:

false
false

AtomicMarkableReference 类多一个 boolean 类型的 mark 标记,只有旧值与 mark 标记全都相等的时候,才能成功 compareAndSet。

可以看源码的这个方法:

    public boolean compareAndSet(V       expectedReference,
                                 V       newReference,
                                 boolean expectedMark,
                                 boolean newMark) {
        Pair<V> current = pair;
        return
            expectedReference == current.reference &&
            expectedMark == current.mark &&
            ((newReference == current.reference &&
              newMark == current.mark) ||
             casPair(current, Pair.of(newReference, newMark)));
    }

不过需要注意的是:在更新时,是否更改 mark 标记全靠自觉,比如上面示例代码中注释掉的那句:

atomicFoo.set(old, marked);

如果不仅把 reference 变量还原,连 mark 标记也还原,那就跟没改过一样,仍然能成功 compareAndSet。

AtomicStampedReference:

AtomicStampedReference 与 AtomicMarkableReference 类似,只不过把 boolean 值换成了 int 值,因此可以用来存储版本号。

public class AtomicStampedReferenceTest {

    private static class Foo {
        public Foo(int i) {
            this.i = i;
        }
        private int i = 0;
    }

    private static AtomicStampedReference<Foo> atomicFoo = new AtomicStampedReference<>(new Foo(1), 1);

    public static void main(String[] args) {
        //线程 1 读取旧值
        int version = atomicFoo.getStamp();
        Foo old = atomicFoo.getReference();
        //模拟线程 2 将 A 改为 B
        atomicFoo.set(new Foo(2), version + 1);
        //模拟线程 1 的 CAS 更新
        boolean success = atomicFoo.compareAndSet(old, new Foo(5), version, version + 1);
        System.out.println(success);
        //模拟线程 2 将 B 再改回 A
        atomicFoo.set(old, version + 1);
        //atomicFoo.set(old, version);
        //模拟线程 1 的 CAS 更新
        success = atomicFoo.compareAndSet(old, new Foo(6), version, version + 1);
        System.out.println(success);
    }
}

与 AtomicMarkableReference 一样,同样需要注意:每次更新的时候,自觉增加版本号,否则 compareAndSet 一样可以成功更新。

4.5.3 数组类型原子类

数组类型原子类有这三个:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray。

它们解决的问题是:对于数组类型,如果使用 AtomicReference,那么只能保证更新数组引用本身的原子性,而不能保证更新数组中各个元素的引用的原子性。

如:

public class AtomicReferenceArrayTest {

    private static String[] strArray = {"x", "y"};
    private static AtomicReference<String[]> atomicReference = new AtomicReference<>(strArray);

    public static void main(String[] args) {
        atomicReference.compareAndSet(strArray, new String[]{});
    }
}

将 String[] 类型包装成 AtomicReference,那么 compareAndSet 就只能对 String[] 类型操作,要对 String[] 中的String 类型的元素进行 compareAndSet 操作,是没有这样的 API 的。

那么就可以用 AtomicReferenceArray:

public class AtomicReferenceArrayTest {

    private static String[] strArray = {"x", "y"};
    private static AtomicReferenceArray<String> atomicReferenceArray = new AtomicReferenceArray<>(strArray);

    public static void main(String[] args) {
        //cas index 为 1 的元素
        atomicReferenceArray.compareAndSet(1, "y", "z");
    }
}

顺便看一下 AtomicReferenceArray 是如何封装 Unsafe 类的:

public class AtomicReferenceArray<E> implements java.io.Serializable {
    private static final long serialVersionUID = -6209656149925076980L;

    private static final Unsafe unsafe;
    private static final int base;
    private static final int shift;
    private static final long arrayFieldOffset;
    private final Object[] array; // must have exact type Object[]

    static {
        try {
            unsafe = Unsafe.getUnsafe();
            arrayFieldOffset = unsafe.objectFieldOffset
                (AtomicReferenceArray.class.getDeclaredField("array"));
            base = unsafe.arrayBaseOffset(Object[].class);
            int scale = unsafe.arrayIndexScale(Object[].class);
            if ((scale & (scale - 1)) != 0)
                throw new Error("data type scale not a power of two");
            shift = 31 - Integer.numberOfLeadingZeros(scale);
        } catch (Exception e) {
            throw new Error(e);
        }
    }
    
    public final boolean compareAndSet(int i, E expect, E update) {
        return compareAndSetRaw(checkedByteOffset(i), expect, update);
    }

    private boolean compareAndSetRaw(long offset, E expect, E update) {
        return unsafe.compareAndSwapObject(array, offset, expect, update);
    }
    
    private long checkedByteOffset(int i) {
        if (i < 0 || i >= array.length)
            throw new IndexOutOfBoundsException("index " + i);

        return byteOffset(i);
    }

    private static long byteOffset(int i) {
        return ((long) i << shift) + base;
    }
    
    //...
}

其它原子类只有一个 offset,它要多两个:base 和 shift。

offset 用来计算 Object[] 数组对象自身的内存地址,base 和 shift 用来计算数组元素的内存地址。

4.5.4 字段类型原子类

字段类型原子类有:AtomicReferenceFieldUpdater,AtomicIntegerFieldUpdater,AtomicLongFieldUpdater。

以 AtomicIntegerFieldUpdater 为例:

public class AtomicUndaterTest {

    private static class Foo {
        public Foo(int i) {
            this.i = i;
        }
        public volatile int i = 0;
    }

    public static void main(String[] args) {
        Foo foo = new Foo(1);
        AtomicIntegerFieldUpdater<Foo> atomicUndater = AtomicIntegerFieldUpdater.newUpdater(Foo.class, "i");
        boolean success = atomicUndater.compareAndSet(foo, 0, 2);
        System.out.println(success);
    }
}

这些 field updater 能更新的字段必须满足两个条件:

  • 能访问;
  • volatile。

前面几种原子类基本够用了,这些 field updater 主要用途大概是当我们想要扩展自定义的 Atomic 类型时,可以不直接使用 Unsafe,用这些 field updater 就可以。毕竟 Unsafe 类确实是非常 unsafe。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值