02. 并发编程 - 原子操作CAS

224 阅读6分钟

并发编程——原子操作

0.1. 什么是原子操作?

原子操作(atomic operation)是指不会被线程调度机制打断的操作。

  • 一个整体,不会被切割
  • 一旦开始,直到结束,不会被切换
  • 顺序不会被打乱

原子.png

0.2. 为什么使用原子操作?

  • 实现轻量级同步
  • CPU指令级别提供支持
  • 场景:对少量资源进行数据更新操作

0.3. 课程概要

  1. 原子操作的实现原理
  2. 基本类型原子更新类
  3. 数组类型原子更新类
  4. 引用类型原子更新类
  5. 对象属性原子更新类
  6. 总结

0.4. 如何学习原子操作的知识?

  • 熟记概念和原理
  • 记住常用类,并练习常用方法
  • 学会查API使用其它功能

0.5. 学习目标

  • 理解原子操作的概念
  • 能够描述出Java原子操作类的基本原理
  • 能够正确使用常见的原子操作类

1. 原子操作的实现原理

1.1. 如何理解操作的原子性?

下面第二行代码,进行了几个动作?

int i = 0;
int x = i + 1; // 几个动作?
  • 读取变量 i 的值
  • 对变量 i 进行加 1 运算
  • 赋值给变量 x
  • 结论:该操作不具有原子性

1.2. 原子操作是如何如何实现的?

  • 处理器保证基本内存操作的原子性

    总线锁

    缓存锁

  • 循环CAS(Compare And Swap,比较并交换)

    Java中的原子操作正是利用了处理器提供的CMPXCHG指令实现的:

    CMPXCHG指令 = 处理器锁 + 循环CAS

1.3. CAS操作的实现思路

01.原子操作的实现思路.png

1.4. CAS操作的问题

  • 只能保证一个共享变量的操作
  • CSA操作长时间不成功
  • ABA问题(A->B->A):版本号

2. 基本类型原子更新类

  • AtomicInteger
  • AtomicLong
  • AtomicBoolean

2.1. 基本类型原子操作类演示:AtomicInteger

2.1.1. 示例代码:


// public static int number = 0; // 共享变量
public static AtomicInteger number = new AtomicInteger(0);
public static void main(String[] args) {
    Runnable runnable = () -> {
        for (int i = 0; i < 1000; i++){
            // synchronized (PrimitiveTypeAtomic.class) {
            //     number++; // number = number + 1
            // }
            // number.getAndIncrement(); // number = number++
            number.incrementAndGet(); // number = ++number
        }
        // System.out.println(Thread.currentThread().getName() + ":" + number);
        System.out.println(Thread.currentThread().getName() + ":" + number.get());

    };
    for (int i = 0; i < 15; i++) {
        new Thread(runnable).start();
    }
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("最终结果" + number.get());
}

2.1.2. 输出结果:

Thread-3:3443
Thread-10:6000
Thread-4:5000
Thread-7:12000
Thread-9:8688
Thread-5:7000
Thread-14:13000
Thread-8:9200
Thread-6:11433
Thread-0:3123
Thread-11:10000
Thread-12:15000
Thread-2:4000
Thread-1:3124
Thread-13:14661
最终结果15000

2.1.3. 结论:

  • 数据准确,不会出现错误
  • 相对于同步机制,效率更高(参考下个小节“并发工具类”中的代码WaitNotifyLatch.java

3. 数组类型原子更新类

  • AtomicIntegerArray
  • AtomicLongArray
  • AtomicReferenceArray

3.1. 数组类型原子操作类演示:AtomicIntegerArray

3.1.1. 示例代码

int[] intArr = {6, 2, 4, 1, 5, 9};
// array.clone();拷贝原数组到类的成员的位置
AtomicIntegerArray atomicArr = new AtomicIntegerArray(intArr);

atomicArr.compareAndSet(3, 1, 7);
System.out.println(atomicArr.get(3)); // 7
System.out.println(intArr[3]); // 1

3.1.2. 输出结果

7
1

3.1.3. 结论

AtomicIntegerArray执行了数组的拷贝,更新相关数据,对原数组没有影响。见下面源码:

public AtomicIntegerArray(int[] array) {
    // Visibility guaranteed by final field guarantees
    this.array = array.clone();
}

3.2. 数组类型原子操作类演示:AtomicReferenceArray

3.2.1. 示例代码

UserInfo user1 = new UserInfo(149, "张三丰");
UserInfo user2 = new UserInfo(80, "东方不败");
UserInfo[] userArr = {user1, user2};

// this.array = Arrays.copyOf(array, array.length, Object[].class);
AtomicReferenceArray<UserInfo> userAtomicArr = new AtomicReferenceArray<>(userArr);
UserInfo user3 = new UserInfo(88, "任我行");
userAtomicArr.compareAndSet(1, user2, user3);
System.out.println(userAtomicArr.get(1));
System.out.println(userArr[1]);
/**
 * 用户信息实体类
 *
 * @author itcast
 */
public class UserInfo {
    private int age;

    private String name;

    volatile String hobby; // 爱好,用于支持对象属性原子更新操作

    public UserInfo(){}

    public UserInfo(int age, String name) {
        this.age = age;
        this.name = name;
    }

    public UserInfo(int age, String name, String hobby) {
        this.age = age;
        this.name = name;
        this.hobby = hobby;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "UserInfo{" +
                "age=" + age +
                ", name='" + name + '\'' +
                '}';
    }
}

3.2.2. 输出结果

UserInfo{age=88, name='任我行'}
UserInfo{age=80, name='东方不败'}

3.2.3. 结论

AtomicReferenceArray执行了数组的拷贝,更新相关数据,对原数组没有影响。见下面源码:

public AtomicReferenceArray(E[] array) {
    // Visibility guaranteed by final field guarantees
    this.array = Arrays.copyOf(array, array.length, Object[].class);
}

4. 引用类型原子更新类

  • AtomicReference
  • AtomicStampedReference
  • AtomicMarkableReference

4.1. 引用类型原子操作类演示:AtomicReference

4.1.1. 示例代码:

UserInfo user = new UserInfo(5, "博学谷");
AtomicReference<UserInfo> atomicUser = new AtomicReference<>();
atomicUser.set(user);

UserInfo newUser = new UserInfo(17, "传智播客");
atomicUser.compareAndSet(user, newUser);

System.out.println(atomicUser.get().getName() + "-" + atomicUser.get().getAge());
System.out.println(user.getName() + "-" + user.getAge());

4.1.2. 输出结果:

传智播客-17
博学谷-5

4.1.3. 结论:

引用类型原子更新操作,将新的对象通过CAS操作赋值给更新器的成员变量,不会对原引用有影响。参加下面源码:

private volatile V value; // 初始化引用,存储在此成员变量上
public final void set(V newValue) {
    // 通过set方法将对象赋值给成员变量,后续的CAS操作,也是赋值给此变量,对原引用没有影响
    value = newValue;
}

4.2. 引用类型原子操作类演示:AtomicStampedReference

演示引用类型带版本戳的原子更新类的操作——带版本戳的引用类型原子更新类:AtomicStampedReference。

演示A - B - A问题:

  1. 首先演示基本操作
  2. 演示版本戳的效果
  3. 演示改回最初引用的效果

4.2.1. 示例代码:

// 1.首先演示基本操作
UserInfo userA = new UserInfo(18, "博小谷");
AtomicStampedReference<UserInfo> stampedUser = new AtomicStampedReference<>(userA, 0);
System.out.println("初始版本号:" + stampedUser.getStamp());
System.out.println("初始引用对象:" + stampedUser.getReference());

// 2.演示版本戳的效果
Runnable exchangeUserRunner = () -> {
    // 使用循环,反复更新AtomicStampedReference中的引用实例
    UserInfo lastUser = userA;
    for (int i = 1; i <= 3; i++) {
        UserInfo userB = new UserInfo(50 + i, "马爸爸" + i);
        stampedUser.compareAndSet(lastUser, userB,
                                  stampedUser.getStamp(), stampedUser.getStamp() + 1);
        lastUser = userB;
    }
};
Thread changeStampedThread = new Thread(exchangeUserRunner);
changeStampedThread.start();
changeStampedThread.join();
System.out.println("---------------------------");
System.out.println("线程1修改后的版本号:" + stampedUser.getStamp());
System.out.println("线程1修改后的引用对象:" + stampedUser.getReference());

// 3.演示改回最初引用的效果
Runnable changeToARunner = () -> {
    stampedUser.compareAndSet(stampedUser.getReference(), userA,
                              stampedUser.getStamp(), stampedUser.getStamp() + 1);
};
Thread changeToAThread = new Thread(changeToARunner);
changeToAThread.start();
changeToAThread.join();
System.out.println("---------------------------");
System.out.println("线程2修改回最初引用后的版本号:" + stampedUser.getStamp());
System.out.println("线程2修改回最初引用后的引用对象:" + stampedUser.getReference());

System.out.println(stampedUser.getReference() == userA);

4.2.2. 输出结果:

初识版本号:0
初识引用对象:UserInfo{age=18, name='博小谷'}
---------------------------
线程1修改后的版本号:3
线程1修改后的引用对象:UserInfo{age=53, name='马爸爸3'}
---------------------------
线程2修改回最初引用后的版本号:4
线程2修改回最初引用后的引用对象:UserInfo{age=18, name='博小谷'}
true

4.2.3. 结论:

每修改一次,版本号加1

每次修改都会通过版本号进行记录,使用AtomicStampedReference类关注更改次数

4.3. 引用类型原子操作类演示:AtomicMarkableReference

4.3.1. 示例代码:

UserInfo userOld = new UserInfo(18, "张三");
UserInfo userNew = new UserInfo(20, "李四");

// 和AtomicStampedReference的区别在于,关注的问题:是否被修改过
AtomicMarkableReference<UserInfo> amrUser =
    new AtomicMarkableReference<>(userOld, false);
// amrUser.compareAndSet(userOld, userNew, true, true); // 修改标记不匹配,无法修改成新值
amrUser.compareAndSet(userOld, userNew, false, false);
System.out.println(amrUser.getReference());
System.out.println(amrUser.isMarked());

4.3.2. 输出结果:

UserInfo{age=20, name='李四'}
false

4.3.3. 结论:

  • 使用AtomicMarkableReference类进行CAS操作,需要传递四个参数:

      1. 旧值(期望值);2. 新值;3. 旧的标记(期望标记);4. 新的标记;
    • 旧值(期望值)与更新器中存储的值不同,更新失败;
    • 旧的标记(期望标记)与更新器中存储的标记不同,更新失败;
    • 新值 == 更新器中存储的值,并且 新的标记 == 更新器中存储的标记,不做任何操作;
    • 否则,执行更新操作
  • 每次修改都会通过版本号进行记录,使用AtomicMarkableReference类关注是否被修改过

5. 对象属性原子更新类

  • AtomicIntegerFieldUpdater
  • AtomicLongFieldUpdater
  • AtomicReferenceFieldUpdater

5.1. 对象属性原子操作类演示:AtomicReferenceFieldUpdater

此类用于更新对象的引用类型属性,整型属性的更新不再赘述。

5.1.1. 示例代码:

// 创建了一个对象属性的原子更新器
AtomicReferenceFieldUpdater fieldUpdater = AtomicReferenceFieldUpdater
                .newUpdater(UserInfo.class, String.class, "hobby");
UserInfo userInfo = new UserInfo(22, "博小谷", "爱编程");
System.out.println(fieldUpdater.get(userInfo)); // 获取指定对象的hobbit属性值

fieldUpdater.getAndUpdate(userInfo, str -> "爱Java");
System.out.println(fieldUpdater.get(userInfo)); // 获取指定对象的hobbit属性值

5.1.2. 输出结果:

爱编程
爱Java

5.1.3. 结论:

  • 使用AtomicReferenceFieldUpdater类进行CAS操作,需要传递三个参数:

    1.要操作的对象的类型;2. 对象的属性类型;3. 属性名称;

  • 泛型

    • T - 要更新的对象的类型
    • V - 要更新的对象的属性的类型
  • 通过反射机制找到对象的属性,然后进行操作

  • 检查访问权限:对象更新器与要操作的属性的访问权限是否匹配

  • 必须是volatile关键字修饰的属性:变量的读一致性(确保每个线程读取该变量时都是最新数据)

  • 要操作的变量不能用static修饰

  • 要操作的变量不能用final修饰

参考源码:

public static <U,W> AtomicReferenceFieldUpdater<U,W> newUpdater(Class<U> tclass,
                                                                Class<W> vclass,
                                                                String fieldName) {
    return new AtomicReferenceFieldUpdaterImpl<U,W>
        (tclass, vclass, fieldName, Reflection.getCallerClass());
}
AtomicReferenceFieldUpdaterImpl(final Class<T> tclass,
                                final Class<V> vclass,
                                final String fieldName,
                                final Class<?> caller) {
    final Field field;
    final Class<?> fieldClass;
    final int modifiers;
    try {
        // 通过反射机制获取属性
        field = AccessController.doPrivileged(
            new PrivilegedExceptionAction<Field>() {
                public Field run() throws NoSuchFieldException {
                    return tclass.getDeclaredField(fieldName);
                }
            });
        modifiers = field.getModifiers();
        sun.reflect.misc.ReflectUtil.ensureMemberAccess(
            caller, tclass, null, modifiers);
        ClassLoader cl = tclass.getClassLoader();
        ClassLoader ccl = caller.getClassLoader();
        if ((ccl != null) && (ccl != cl) &&
            ((cl == null) || !isAncestor(cl, ccl))) {
            sun.reflect.misc.ReflectUtil.checkPackageAccess(tclass); // 访问权限
        }
        fieldClass = field.getType();
    } catch (PrivilegedActionException pae) {
        throw new RuntimeException(pae.getException());
    } catch (Exception ex) {
        throw new RuntimeException(ex);
    }

    if (vclass != fieldClass)
        throw new ClassCastException();
    if (vclass.isPrimitive()) // 不能是基本数据类型
        throw new IllegalArgumentException("Must be reference type");

    if (!Modifier.isVolatile(modifiers)) // 必须是volatile修饰
        throw new IllegalArgumentException("Must be volatile type");

    // Access to protected field members is restricted to receivers only
    // of the accessing class, or one of its subclasses, and the
    // accessing class must in turn be a subclass (or package sibling)
    // of the protected member's defining class.
    // If the updater refers to a protected field of a declaring class
    // outside the current package, the receiver argument will be
    // narrowed to the type of the accessing class.
    this.cclass = (Modifier.isProtected(modifiers) &&
                   tclass.isAssignableFrom(caller) &&
                   !isSamePackage(tclass, caller))
        ? caller : tclass;
    this.tclass = tclass;
    this.vclass = vclass;
    this.offset = U.objectFieldOffset(field); // 内存操作
}
// 获取对象属性(Field实例)的偏移量(地址值)
public long objectFieldOffset(Field f) {
    if (f == null) {
        throw new NullPointerException();
    }
	// 仅支持操作非静态属性。获取静态属性偏移量使用:staticFieldOffset(f)
    return objectFieldOffset0(f);
}

5.2. volatile关键字

5.2.1. volatile关键字的基本概念

  1. 含义:易变的,不稳定的

  2. 作用:

    用于修饰变量;

    禁止编译器优化:让线程每次都从主内存中读/写数据,修改完成后,立即写回

    使用场景:一个线程写,多个线程读

  3. 注意事项(误区):

    volatile是非线程安全的,并不是用来实现同步的

    保证变量的可见性:线程每次读取都是最新的值(因为每次都是从主内存中读取)

    不能保证原子性:多线程操作volatile修饰的变量,不能保证原子性

5.2.2. volatile关键字的工作原理

02.volatile关键字的工作原理.gif

5.2.3. volatile关键字可以保证可见性

示例代码:
// private static int num = 10; // 共享变量
private volatile static int num = 10; // 共享变量

public static void main(String[] args) {
    // 多个线程读
    Runnable reader = () -> {
        while(num == 10) {
            // nothing to do
        }
        String name = Thread.currentThread().getName();
        System.out.println("线程" + name + "读取到的是:" + num);
    };
    for (int i = 1; i <= 3; i++) {
        new Thread(reader).start();
    }
    try {
        System.out.println("主线程休眠3秒...");
        Thread.sleep(3000);
        System.out.println("主线程休眠结束...");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    // 一个线程写
    new Thread(() -> {
        num = 15;
        System.out.println("写线程完成写入。。。");
    }).start();
}
输出结果:
主线程休眠3秒...
主线程休眠结束...
写线程完成写入。。。
线程Thread-0读取到的是:15
线程Thread-1读取到的是:15
线程Thread-2读取到的是:15
结论:

线程获取到主内存中的数据之后,会放到线程本地存储(ThreadLocalStorage,寄存器中),下次使用直接从线程本地存储中获取,要比从主内存中获取数据的效率高很多。当主内存中的变量用volatile修饰之后,线程每次使用数据,都必须重新读取主内存,这样就能够保证多个线程间获取的数据是一致的。volatile关键字使缓存失效,来确保可见性。

volatile关键字可以保证可见性。

5.2.3. volatile关键字不能保证原子性

示例代码:
private static volatile int num = 0;

public static void main(String[] args) {
    Runnable add = () -> {
        for (int i = 0; i < 1000; i++) {
            num++; // 非原子操作
        }
        System.out.println(Thread.currentThread().getName() + "执行结果是:" + num);
    };
    for (int i = 0; i < 3; i++) {
        new Thread(add).start();
    }
    try {
        Thread.sleep(3000);
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
    System.out.println("最终运算结果:" + num);
}
输出结果:
Thread-1执行结果是:1663
Thread-0执行结果是:2352
Thread-2执行结果是:1214
最终运算结果:2352
结论:

多线程操作共享变量时,由于该操作非原子操作,该操作的执行过程的任何一个时间点都有可能被CPU剥夺执行权,其它线程拿到执行权之后,获取到的数据可能还是前一个线程未完成写回的原数据,造成同一个值被多个线程多次读取,最终结果小于预期值。

volatile关键字不能保证原子性。

5.3. AtomicLong与LongAdder的区别

原理:

03.AtomicLong与LongAdder的区别.gif

示例代码:
AtomicLong atomicLong = new AtomicLong();
LongAdder longAdder = new LongAdder();

long start = System.currentTimeMillis(); // 计时开始
Runnable adder = () -> {
    for (int j = 0; j < 1000000; j++) {
        // TODO 分别执行AtomicLong和LongAdder的自增方法
        //                atomicLong.incrementAndGet();
        longAdder.increment();
    }
};
for (int i = 0; i < 50; i++) {
    new Thread(adder).start();
}

while (Thread.activeCount() > 2) { // ctrl-break; main;
}
// TODO 打印运算结果
//        System.out.println("运算结果是:" + atomicLong.get());
System.out.println("运算结果是:" + longAdder.sum());

System.out.println("耗时:" + (System.currentTimeMillis() - start));
输出结果:
// LongAdder的输出结果
运算结果是:50000000
耗时:244

// LongAdder的输出结果
运算结果是:50000000
耗时:1130
结论:

将一个变量拆分成多个变量,多个线程各自操作自己的数据,大大减少多线程竞争CAS操作的情况,降低发生自旋的概率,从而提高效率。

在运算量较少的情况下,AtomicLong的性能可能会好于LongAdder,所以,后者适用于超高运算量的场景。