CAS操作

CAS的定义

CAS的全称为“Compare-And-Swap”,直译就是对比并交换。是一条CPU的原子指令,其作用是让CPU先进行比较两个值是否相等,然后原子地更新某个位置的值。其实现方式是基于硬件平台的汇编指令,就是说CAS是靠硬件实现的,JVM只是封装了汇编调用,如AtomicInteger类便是使用了这些封装后的接口。CAS的简单解释:CAS操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较下在旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换。CAS操作是原子性的,所以多线程并发使用CAS更新数据时,可以不使用锁。JDK中大量使用了CAS来更新数据而防止加锁(synchronized 重量级锁)来保持原子更新。

  1. public interface Account {
  2. // 获取余额
  3. Integer getBalance();
  4. // 取款
  5. void withdraw(Integer amount);
  6. /**
  7. * 方法内会启动 1000 个线程,每个线程做 -10 元 的操作
  8. * 如果初始余额为 10000 那么正确的结果应当是 0
  9. */
  10. static void demo(Account account) {
  11. List<Thread> ts = new ArrayList<>();
  12. long start = System.nanoTime();
  13. for (int i = 0; i < 1000; i++) {
  14. ts.add(new Thread(() -> {
  15. account.withdraw(10);
  16. }));
  17. }
  18. ts.forEach(Thread::start);
  19. ts.forEach(t -> {
  20. try {
  21. t.join();
  22. } catch (InterruptedException e) {
  23. e.printStackTrace();
  24. }
  25. });
  26. long end = System.nanoTime();
  27. System.out.println(account.getBalance()
  28. + " cost: " + (end - start) / 1000_000 + " ms");
  29. }
  30. public static void main(String[] args) {
  31. Account.demo(new AccountCas(10000));
  32. }
  33. }

前面的学习中,可以使用加锁的方式来保证线程安全,如:

  1. public class AccountLock implements Account{
  2. private Integer balance;
  3. public AccountLock(Integer balance) {
  4. this.balance = balance;
  5. }
  6. @Override
  7. public Integer getBalance() {
  8. return balance;
  9. }
  10. @Override
  11. public void withdraw(Integer amount) {
  12. balance -= amount;
  13. }
  14. }

但是,如果不加锁,怎么确保线程安全?其实,线程安全问题就是出在共享变量“Integer balance”身上,Java中其实提供了原子操作的Interger类:AtomicInteger,它可以确保线程安全,如:

  1. public class AccountCas implements Account {
  2. private final AtomicInteger balance;
  3. public AccountCas(Integer balance) {
  4. this.balance = new AtomicInteger(balance);
  5. }
  6. @Override
  7. public Integer getBalance() {
  8. return balance.get();
  9. }
  10. @Override
  11. public void withdraw(Integer amount) {
  12. while (true) {
  13. int prev = balance.get();
  14. int next = prev - amount;
  15. //compareAndSet 正是做这个检查,在 set 前,先比较 prev 与当前值
  16. // - 不一致了,next 作废,返回 false 表示失败
  17. // 比如,别的线程已经做了减法,当前值已经被减成了 990
  18. // 那么本线程的这次 990 就作废了,进入 while 下次循环重试
  19. // - 一致,以 next 设置为新值,返回 true 表示成功
  20. if (balance.compareAndSet(prev, next)) {
  21. break;
  22. }
  23. }
  24. // 可以简化为下面的方法
  25. // balance.addAndGet(-1 * amount);
  26. }
  27. }

image.png
可以看到,我们没有加锁,依然保证了线程安全。其原因就是在于“compareAndSet”这个方法,其实现就是采用的CAS操作实现的。CAS其实就是“compareAndSet”的缩写,当然,也有另一种说法是“CompareAndSwap”的缩写,不过表达的意思都是一样:在修改共享变量前,会将工作内存的变量和主存的变量进行比较(compare),如果一样就进行修改(Set或Swap),如果不一样则放弃修改并进入下一轮循环(如上面的例子),直到一样为止,示意图如下所示:
image.png
这样,就可以保证变量balance的线程安全。

CAS与锁的比较

性能的比较

CAS和加锁可以达到一样的效果,而且通过上面的例子比较,CAS的性能甚至会高于加锁,原因解释如下面的比喻:
无锁情况下,即使重试失败,线程始终在高速运行,没有停歇,而 synchronized 会让线程在没有获得锁的时候,发生上下文切换,进入阻塞。线程就好像高速跑道上的赛车,高速运行时,速度超快,一旦发生上下文切换,就好比赛车要减速、熄火,等被唤醒又得重新打火、启动、加速… 恢复到高速运行,代价比较大。但无锁情况下,因为线程要保持运行,需要额外 CPU 的支持,CPU 在这里就好比高速跑道,没有额外的跑道,线程想高速运行也无从谈起,虽然不会进入阻塞,但由于没有分到时间片,仍然会进入可运行状态,还是会导致上下文切换。
当前,性能优于加锁是有前提条件的:【适用于线程数少、多核 CPU 的场景下】。如果只有单核CPU,没有多余的CPU核心去维持在高速运转的CAS线程;如果运行的线程很多,可以想到重试必然频繁发生,反而效率会受影响。

实现思想的比较

image.png

CAS与volatile

CAS操作的实现其实是需要依赖volatile这个关键字的,因为需要和主存的变量进行比较,这就需要volatile修饰哪个变量了。volatile可以用来修饰成员变量和静态成员变量,他可以避免线程从自己的工作缓存中查找变量的值,必须到主存中获取它的值,线程操作 volatile 变量都是直接操作主存。CAS 必须借助 volatile 才能读取到共享变量的最新值来实现比较并交换的效果。

CAS的一些问题

ABA问题

image.png

AtomicStampedReference的使用示例如下:

  1. @Slf4j(topic = "c.main")
  2. public class Main {
  3. private static AtomicStampedReference<Integer> atomicStampedRef =
  4. new AtomicStampedReference<>(1, 0);
  5. public static void main(String[] args) {
  6. Thread main = new Thread(() -> {
  7. System.out.println("操作线程" + Thread.currentThread() + ",初始值 a = " + atomicStampedRef.getReference());
  8. int stamp = atomicStampedRef.getStamp(); //获取当前标识别
  9. try {
  10. Thread.sleep(1000); //等待1秒 ,以便让干扰线程执行
  11. } catch (InterruptedException e) {
  12. e.printStackTrace();
  13. }
  14. boolean isCASSuccess = atomicStampedRef.compareAndSet(1, 2, stamp, stamp + 1); //此时expectedReference未发生改变,但是stamp已经被修改了,所以CAS失败
  15. System.out.println("操作线程" + Thread.currentThread() + ",CAS操作结果: " + isCASSuccess);
  16. }, "主操作线程");
  17. Thread other = new Thread(() -> {
  18. Thread.yield(); // 确保thread-main 优先执行
  19. atomicStampedRef.compareAndSet(1, 2, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
  20. System.out.println("操作线程" + Thread.currentThread() + ",【increment】 ,值 = " + atomicStampedRef.getReference());
  21. atomicStampedRef.compareAndSet(2, 1, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
  22. System.out.println("操作线程" + Thread.currentThread() + ",【decrement】 ,值 = " + atomicStampedRef.getReference());
  23. }, "干扰线程");
  24. main.start();
  25. other.start();
  26. }
  27. }

image.png

循环时间长开销大

image.png

只能保证一个共享变量的原子操作

image.png

13个原子操作类

Atomic包下的原子操作类

Java 从 JDK 1.5 开始提供了 java.util.concurrent.atomic 包(简称Atomic包),这个包中的原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。Atoic包下提供了12个类,他们分别可以实现四种原子操作:原子更新基本类型、原子更新数组、原子更新引用、原子更新属性(字段)。

原子更新基本类型

image.png
实例代码:

  1. public static void main(String[] args) {
  2. AtomicInteger ai = new AtomicInteger(1);
  3. System.out.println("ai.get() = " + ai.get());
  4. System.out.println("ai.addAndGet(5) = " + ai.addAndGet(5));
  5. System.out.println("ai.get() = " + ai.get());
  6. System.out.println("ai.compareAndSet(ai.get(), 10) = " + ai.compareAndSet(ai.get(), 10));
  7. System.out.println("ai.get() = " + ai.get());
  8. System.out.println("ai.getAndIncrement() = " + ai.getAndIncrement());
  9. System.out.println("ai.get() = " + ai.get());
  10. ai.lazySet(8);
  11. System.out.println("ai.lazySet(8)");
  12. System.out.println("ai.get() = " + ai.get());
  13. System.out.println("ai.getAndSet(5) = " + ai.getAndSet(5));
  14. System.out.println("ai.get() = " + ai.get());
  15. }

image.png

原子更新引用类型

image.png
以AtomicReference为例说明:

  1. public class Main {
  2. public static AtomicReference<User> atomicUserRef = new AtomicReference<>();
  3. public static void main(String[] args) {
  4. User user = new User("conan", 15);
  5. atomicUserRef.set(user);
  6. User updateUser = new User("Shinichi", 17);
  7. atomicUserRef.compareAndSet(user, updateUser);
  8. System.out.println(atomicUserRef.get().getName());
  9. System.out.println(atomicUserRef.get().getOld());
  10. }
  11. @Data
  12. @AllArgsConstructor
  13. @NoArgsConstructor
  14. static class User {
  15. private String name;
  16. private int old;
  17. }
  18. }

image.png

原子更新字段类

image.png
如:

  1. public class Main {
  2. //创建一个原子更新器
  3. private static final AtomicIntegerFieldUpdater<User> ATOMIC_INTEGER_FIELD_UPDATER =
  4. AtomicIntegerFieldUpdater.newUpdater(User.class, "old");
  5. public static void main(String[] args) {
  6. User user = new User("Tom", 15);
  7. //原来的年龄
  8. System.out.println(ATOMIC_INTEGER_FIELD_UPDATER.getAndIncrement(user));
  9. //现在的年龄
  10. System.out.println(ATOMIC_INTEGER_FIELD_UPDATER.get(user));
  11. }
  12. @Data
  13. @AllArgsConstructor
  14. @NoArgsConstructor
  15. static class User {
  16. private String name;
  17. public volatile int old;
  18. }
  19. }

image.png

原子更新数组

image.png
示例代码:

  1. public class Main {
  2. static int[] value = new int[]{1, 2};
  3. static AtomicIntegerArray ai = new AtomicIntegerArray(value);
  4. public static void main(String[] args) {
  5. ai.getAndSet(1, 300);
  6. System.out.println(ai.get(1));
  7. System.out.println(value[1]);
  8. }
  9. }

image.png

Unsafe类

Unsafe类简介

Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。但由于Unsafe类使Java语言拥有了类似C语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用Unsafe类会使得程序出错的概率变大,使得Java这种安全的语言变得不再“安全”,因此对Unsafe的使用一定要慎重。这个类尽管里面的方法都是 public 的,但是并没有办法使用它们,JDK API 文档也没有提供任何关于这个类的方法的解释。总而言之,对于 Unsafe 类的使用都是受限制的,只有授信的代码才能获得该类的实例,当然 JDK 库里面的类是可以随意使用的。
image.png
如上图所示,Unsafe提供的API大致可分为内存操作、CAS、Class相关、对象操作、线程调度、系统信息获取、内存屏障、数组操作等几类。CAS的API的详细用法参考文章:
https://blog.csdn.net/muyimo/article/details/121379139

获取Unsafe类

Unsafe 对象提供了非常底层的,操作内存、线程的方法,Unsafe 对象不能直接调用,只能通过反射获得。

  1. public class Main {
  2. static Unsafe unsafe;
  3. static {
  4. try {
  5. Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
  6. theUnsafe.setAccessible(true);
  7. unsafe = (Unsafe) theUnsafe.get(null);
  8. } catch (NoSuchFieldException | IllegalAccessException e) {
  9. throw new Error(e);
  10. }
  11. }
  12. static Unsafe getUnsafe() {
  13. return unsafe;
  14. }
  15. public static void main(String[] args) {
  16. System.out.println(getUnsafe());
  17. }
  18. }

image.png

CAS操作

  1. public class Main {
  2. static Unsafe unsafe;
  3. static {
  4. try {
  5. Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
  6. theUnsafe.setAccessible(true);
  7. unsafe = (Unsafe) theUnsafe.get(null);
  8. } catch (NoSuchFieldException | IllegalAccessException e) {
  9. throw new Error(e);
  10. }
  11. }
  12. static Unsafe getUnsafe() {
  13. return unsafe;
  14. }
  15. public static void main(String[] args) throws NoSuchFieldException {
  16. Unsafe unsafe = getUnsafe();
  17. Field id = Student.class.getDeclaredField("id");
  18. Field name = Student.class.getDeclaredField("name");
  19. // 获得成员变量的偏移量
  20. long idOffset = unsafe.objectFieldOffset(id);
  21. long nameOffset = unsafe.objectFieldOffset(name);
  22. Student student = new Student();
  23. // 使用 cas 方法替换成员变量的值
  24. unsafe.compareAndSwapInt(student, idOffset, 0, 20); // 返回 true
  25. unsafe.compareAndSwapObject(student, nameOffset, null, "张三"); // 返回 true
  26. System.out.println(student);
  27. }
  28. }
  29. @Data
  30. class Student {
  31. volatile int id;
  32. volatile String name;
  33. }

image.png
因此,利用Unsafe类,可以自己实现一个简单的AtomicInteger类,如:

  1. public class AtomicData {
  2. private volatile int data;
  3. static final Unsafe unsafe;
  4. static final long DATA_OFFSET;
  5. static {
  6. unsafe = Main.getUnsafe();
  7. try {
  8. // data 属性在 DataContainer 对象中的偏移量,用于 Unsafe 直接访问该属性
  9. DATA_OFFSET = unsafe.objectFieldOffset(AtomicData.class.getDeclaredField("data"));
  10. } catch (NoSuchFieldException e) {
  11. throw new Error(e);
  12. }
  13. }
  14. public AtomicData(int data) {
  15. this.data = data;
  16. }
  17. public void decrease(int amount) {
  18. int oldValue;
  19. while (true) {
  20. if (unsafe.compareAndSwapInt(this, DATA_OFFSET, oldValue, oldValue - amount)) {
  21. return;
  22. }
  23. }
  24. }
  25. public int getData() {
  26. return data;
  27. }
  28. }