本次内容主要讲原子操作的概念、原子操作的实现方式、CAS的使用、原理、3大问题及其解决方案,最后还讲到了JDK中经常使用到的原子操作类。

1、什么是原子操作?

  所谓原子操作是指不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会有任何线程上下文切换。原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分。我们常用的i++看起来虽然简单,但这并不是一个原子操作,具体原理后面单独介绍。假定有两个操作A和B,如果从执行A的线程来看,当另一个线程执行B时,要么将B全部执行完,要么完全不执行B,那么A和B对彼此来说是原子的。将整个操作视作一个整体是原子性的核心特征。

2、如何实现原子操作?

2.1 锁机制实现原子操作及其问题

实现原子操作可以使用锁。锁机制满足基本的需求是没有问题的,但是有的时候我们的需求并非这么简单,我们需要更有效,更加灵活的机制。synchronized关键字是基于阻塞的锁机制,也就是说当一个线程拥有锁的时候,访问同一资源的其它线程需要等待,直到该线程释放锁。使用synchronized关键字存在这样的问题:

(1)如果被阻塞的线程优先级很高很重要怎么办?

(2)如果获得锁的线程一直不释放锁怎么办?

(3)如果有大量的线程来竞争资源,那CPU将会花费大量的时间和资源来处理这些竞争,同时,还有可能出现一些例如死锁之类的情况。

使用锁机制是一种比较粗糙、粒度比较大的机制,我们可以想象多个线程操作同一个计数器的业务场景,使用锁机制的话显得太过笨重。

2.2 CAS机制

  实现原子操作还可以使用当前的处理器基本都支持CAS(Compare And Swap)的指令,CPU指令集上提供了CAS操作相关指令,实现原子操作可以使用这些指令。每一个CAS操作过程都包含3个运算参数:一个内存地址V,一个期望的值A和一个新值B,操作的时候如果这个地址上存放的值等于这个期望的值A,则将地址上的值赋为新值B,否则不做任何操作。

2.3 CAS使用

  先来模拟一个多个线程操作同一个计数器的场景,JDK中提供了boolean、int和long基本类型对应的原子包装类AtomicBoolean、AtomicInteger和AtomicLong。我们用AtomicInteger演示,通过CountDownLatch进行并发模拟,如果对CountDownLatch用法不了解,欢迎查看上一篇文章,有通俗易懂的例子。先对AtomicInteger的主要API做一个介绍:

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

(2)boolean compareAndSet(int expect,int update):如果当前数值等于expect,则以原子方式将当前值设置为update。

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

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

  1. import java.util.concurrent.CountDownLatch;
  2. import java.util.concurrent.atomic.AtomicInteger;
  3.  
  4. public class AtomicIntegerDemo {
  5. static AtomicInteger counter = new AtomicInteger(0);
  6.  
  7. static CountDownLatch countDownLatch = new CountDownLatch(20);
  8.  
  9. static class CounterThread implements Runnable {
  10. @Override
  11. public void run() {
  12. try {
  13. countDownLatch.await();
  14. } catch (InterruptedException e) {
  15. e.printStackTrace();
  16. }
  17. counter.getAndIncrement();
  18. }
  19. }
  20.  
  21. public static void main(String[] args) throws InterruptedException {
  22. for (int i = 0; i < 20; i++) {
  23. Runnable thread = new CounterThread();
  24. new Thread(thread).start();
  25. countDownLatch.countDown();
  26. }
  27. Thread.sleep(2000); //保证子线程全部执行完成
  28. System.out.println("20个线程并发执行getAndIncrement()方法后的结果:" + counter.get());
  29. counter.compareAndSet(20, 18);//如果counter当前数值为20,则以原子方式更新为18
  30. System.out.println("compareAndSet(20, 18)后的结果:" + counter.get());
  31. }
  32. }

程序中模拟了20个线程并发对一个计数器进行自增操作,结果输出为20,可以看到这段代码并没有用任何的锁,也达到了原子操作目的。

2.4 CAS原理

  CAS的基本思路就是,如果内存地址V上的值和期望的值A相等,则给其赋予新值B,否则不做任何事儿。CAS就是在一个循环里不断的做CAS操作,直到成功为止。CAS是怎么实现线程的安全呢?语言层面不做处理,JDK 调用这些指令来完成CAS操作,本质上就是将其交给CPU和内存,利用CPU的多处理能力,实现硬件层面的阻塞,再加上volatile变量的特性即可实现基于原子操作的线程安全。用一张图来说明。

3、CAS实现原子操作的三大问题

3.1 ABA问题

  因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。举个通俗易懂的例子,我的同事老王今年35岁了,还没有女朋友,我问他有什么要求,给他介绍一个女朋友。老王就说了,只要是没有结婚、35岁以下的女的就行。于是我就给他介绍了一个28岁,刚刚离婚不久的女同志,他还感谢了我好久,可能是他现在都还不知道他这个女朋友离过婚。这就是典型的ABA问题,只关心当前状态,而不管中间经历了什么。ABA问题的解决思路就是使用版本号。给变量追加一个版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。就好比老王的要求改成:35岁以下,没有结婚并且离婚次数为0的女性,就不会发生刚刚的事情了。

3.2 循环时间长开销大。

  CAS自旋如果长时间不成功,会给CPU带来非常大的执行开销。

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

  当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。怎么解决这个问题呢?从Java 1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。

4、JDK中相关原子操作类

4.1 AtomicReference

  AtomicReference,可以原子更新的对象引用。AtomicReference有一个compareAndSet()方法,它可以将已持有引用与预期引用进行比较,如果它们相等,则在AtomicReference对象内设置一个新的引用。看一段代码:

  1. import java.util.concurrent.atomic.AtomicReference;
  2.  
  3. public class AtomicReferenceDemo {
  4. static AtomicReference<UserInfo> atomicReference;
  5.  
  6. public static void main(String[] args) {
  7. //原引用
  8. UserInfo oldUser = new UserInfo("老王", 35);
  9. atomicReference = new AtomicReference<>(oldUser);
  10.  
  11. //新引用
  12. UserInfo updateUser = new UserInfo("小宋", 21);
  13. atomicReference.compareAndSet(oldUser, updateUser);
  14.  
  15. System.out.println("使用compareAndSet()替换原有引用后的结果:" + atomicReference.get());
  16. System.out.println("原引用:" + oldUser);
  17. }
  18.  
  19. static class UserInfo {
  20. private String name;
  21. private int age;
  22.  
  23. public UserInfo(String name, int age) {
  24. this.name = name;
  25. this.age = age;
  26. }
  27.  
  28. public String getName() {
  29. return name;
  30. }
  31.  
  32. public int getAge() {
  33. return age;
  34. }
  35.  
  36. public void setName(String name) {
  37. this.name = name;
  38. }
  39.  
  40. public void setAge(int age) {
  41. this.age = age;
  42. }
  43.  
  44. @Override
  45. public String toString() {
  46. return "UserInfo{" +
  47. "name='" + name + '\'' +
  48. ", age=" + age +
  49. '}';
  50. }
  51. }
  52. }

从程序输出可以看到,atomicReference的持有的引用被修改了,但是原引用对象并没有发生改变。

4.2 AtomicStampedReference

  AtomicStampedReference,利用版本戳的形式记录了每次改变以后的版本号,这样的话就不会存在ABA问题了。 AtomicStampedReference有一个内部类Pair,使用Pair的int stamp作为计数器使用,看下Pair的源码:

  1. private static class Pair<T> {
  2. final T reference;
  3. final int stamp;
  4. private Pair(T reference, int stamp) {
  5. this.reference = reference;
  6. this.stamp = stamp;
  7. }
  8. static <T> Pair<T> of(T reference, int stamp) {
  9. return new Pair<T>(reference, stamp);
  10. }
  11. }

还是老王那个例子,如果使用AtomicStampedReference的话,老王更关心的是介绍的女朋友离过几次婚。用一段代码来模拟给老王介绍女朋友的场景:

  1. import java.util.concurrent.atomic.AtomicStampedReference;
  2.  
  3. public class AtomicStampedReferenceDemo {
  4. static AtomicStampedReference<String> asr = new AtomicStampedReference("介绍的女朋友", 0);
  5.  
  6. public static void main(String[] args) throws InterruptedException {
  7. final String oldReference = asr.getReference();//初始值,表示介绍的女朋友
  8. final int oldStamp = asr.getStamp();//初始版本0,表示介绍的女朋友没有离过婚
  9.  
  10. Thread thread1 = new Thread(() -> {
  11. String newReference = oldReference + " 离婚1次";
  12. boolean first = asr.compareAndSet(oldReference, newReference,
  13. oldStamp, oldStamp + 1);
  14. if (first) {
  15. System.out.println("介绍的女朋友第一次离婚。。。");
  16. }
  17.  
  18. boolean second = asr.compareAndSet(newReference, oldReference + "又离婚了",
  19. oldStamp + 1, oldStamp + 2);
  20. if (second) {
  21. System.out.println("介绍的女朋友第二次离婚。。。");
  22. }
  23. }, "介绍的女朋友离婚");
  24.  
  25. Thread thread2 = new Thread(() -> {
  26. String reference = asr.getReference();//介绍的女朋友最新状态
  27.  
  28. //判断介绍的女朋友最新状态是否符合老王的要求
  29. boolean flag = asr.compareAndSet(reference, reference + "没有离过婚",
  30. oldStamp, oldStamp + 1);
  31. if (flag) {
  32. System.out.println("老王笑嘻嘻地对我说,介绍的女朋友符合我的要求");
  33. } else {
  34. System.out.println("老王拳头紧握地对我说,介绍的女朋友居然离过" + asr.getStamp() + "次婚,不符合我要求!!!!");
  35. }
  36. }, "老王相亲");
  37. thread1.start();
  38. thread1.join();
  39. thread2.start();
  40. thread2.join();
  41. }
  42. }

启动2个子线程,分别代表介绍的女朋友多次离婚以及老王相亲的场景。从程序输出可以看到,介绍的女朋友不符合老王的要求,老王为了避免喜当爹,果断拒绝了。

老王判断的依据是,介绍的女朋友应该是没有离过婚,stamp值等于0才对。但是老王仔细一看,stamp已经是2,不符合我的要求,不能要。

4.3 AtomicMarkableReference

  AtomicMarkableReference,可以原子更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef,booleaninitialMark)。AtomicMarkableReference也有一个内部类Pair,使用Pair的boolean mark来标记状态。还是老王那个例子,使用AtomicStampedReference可能关心的是离婚次数,AtomicMarkableReference关心的是有没有离过婚。用一段代码来模拟:

  1. import java.util.concurrent.atomic.AtomicMarkableReference;
  2.  
  3. public class AtomicMarkableReferenceDemo {
  4. static AtomicMarkableReference markableReference;
  5.  
  6. public static void main(String[] args) throws InterruptedException {
  7. String girl = "介绍的女朋友";
  8. markableReference = new AtomicMarkableReference(girl, false);
  9. Thread t1 = new Thread(() -> {
  10. markableReference.compareAndSet(girl, girl + "离婚", false, true);
  11. System.out.println(markableReference.getReference());
  12. }, "介绍的女朋友离婚了");
  13.  
  14. Thread t2 = new Thread(() -> {
  15. //老王检查标记,只关心这个标志位
  16. boolean marked = markableReference.isMarked();
  17. if (marked) {
  18. System.out.println("你给我介绍的女朋友离过婚,我不要!!");
  19. } else {
  20. System.out.println("兄弟,大兄弟,亲生兄弟啊!!这个女朋友我要了");
  21. }
  22. }, "老王鉴定介绍的女朋友有没有离过婚");
  23.  
  24. t1.start();
  25. t1.join();
  26.  
  27. t2.start();
  28. t2.join();
  29. }
  30. }

程序输出可以看到,老王还是坚持了自己的原则。

4.4 AtomicIntegerArray

  AtomicIntegerArray,元素可以原子更新的数组。其常用方法如下:

(1)int addAndGet(int i,int delta):以原子方式将输入值与数组中索引i的元素相加。

(2)boolean compareAndSet(int i,int expect,int update):如果当前值等于预期值,则以原子方式将数组位置i的元素设置成update值。

需要注意的是,数组value通过构造方法传递进去,然后AtomicIntegerArray会将当前数组复制一份,所以当AtomicIntegerArray对内部的数组元素进行修改时,不会影响传入的数组。

用法比较简单,看一个例子:

  1. public class AtomicIntegerArrayDemo {
  2. static int[] value = new int[]{1, 2};//原始数组
  3. static AtomicIntegerArray atomicIntegerArray = new AtomicIntegerArray(value);
  4.  
  5. public static void main(String[] args) {
  6. atomicIntegerArray.getAndSet(0, 3);
  7. System.out.println("atomicIntegerArray的第一个元素:" + atomicIntegerArray.get(0));
  8. System.out.println("原始数组的第一个元素:" + value[0]);//原数组不会变化
  9. }
  10. }

程序输出可以看到,原始数组并没有受到影响。

顺便看一下AtomicIntegerArray的构造方法:

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

5、结语

  文中例子纯属虚构,便于对知识点的理解,不掺杂任何其他意思。下一篇内容中会介绍Java的显示锁Lock相关知识点,阅读过程中如发现描述有误,请指出,谢谢。

java原子操作CAS的更多相关文章

  1. java并发编程系列二:原子操作/CAS

    什么是原子操作 不可被中断的一个或者一系列操作 实现原子操作的方式 Java可以通过锁和循环CAS的方式实现原子操作 CAS( Compare And Swap )  为什么要有CAS? Compar ...

  2. 【Java】CAS的乐观锁实现之AtomicInteger源码分析

    1. 悲观锁与乐观锁 我们都知道,cpu是时分复用的,也就是把cpu的时间片,分配给不同的thread/process轮流执行,时间片与时间片之间,需要进行cpu切换,也就是会发生进程的切换.切换涉及 ...

  3. Java中CAS原理详解

    在JDK 5之前Java语言是靠synchronized关键字保证同步的,这会导致有锁 锁机制存在以下问题: (1)在多线程竞争下,加锁.释放锁会导致比较多的上下文切换和调度延时,引起性能问题. (2 ...

  4. Java中CAS详解

    在JDK 5之前Java语言是靠synchronized关键字保证同步的,这会导致有锁 锁机制存在以下问题: (1)在多线程竞争下,加锁.释放锁会导致比较多的上下文切换和调度延时,引起性能问题. (2 ...

  5. Java原子操作类,你知道多少?

    原子操作类简介 由于synchronized是采用的是悲观锁策略,并不是特别高效的一种解决方案. 实际上,在J.U.C下的atomic包提供了一系列的操作简单,性能高效,并能保证线程安全的类去 更新基 ...

  6. 多线程之:java的CAS操作的相关信息

    一:锁机制存在的性能问题? 在JDK 5之前Java语言是靠synchronized关键字保证同步的,这会导致有锁(后面的章节还会谈到锁). 锁机制存在以下问题:(1)在多线程竞争下,加锁.释放锁会导 ...

  7. Java性能 -- CAS乐观锁

    synchronized / Lock / CAS synchronized和Lock实现的同步锁机制,都属于悲观锁,而CAS属于乐观锁 悲观锁在高并发的场景下,激烈的锁竞争会造成线程阻塞,而大量阻塞 ...

  8. Java 并发系列之九:java 原子操作类Atomic(13个)

    1. 原子更新基本类型类 2. 原子更新数组 3. 原子更新引用 4. 原子更新属性 5. txt java 原子操作类Atomic 概述 java.util.concurrent.atomic里的原 ...

  9. java面试-CAS底层原理

    一.CAS是什么? 比较并交换,它是一条CPU并发原语. CAS是一种无锁算法,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B.当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什 ...

随机推荐

  1. 源码中TODO、FIXME和XXX的含义

    前言: 今天在阅读Qt  Creator的源代码时,发现一些注释中有FIXME英文单词,用英文词典居然查不到其意义! 实际上,在阅读一些开源代码时,我们常会碰到诸如:TODO.FIXME和XXX的单词 ...

  2. mysql首次使用过程以及彻底卸载过程

    安装过程: 步骤一: 安装mysql服务,使用命令行: yum install mysql-server 步骤二: 启动mysql服务: service mysqld start 确认msyql是否启 ...

  3. Java开发面试常见问题合集

    次面试事故 面试官:你看过哪些源码?我:都挺熟悉的面试官:对hashMap了解程度怎么样?面试官:那你能讲讲 HashMap的实现原理吗?面试官:HashMap什么时候会进行 rehash?面试官:结 ...

  4. python中的reduce函数

    python中的reduce   python中的reduce内建函数是一个二元操作函数,他用来将一个数据集合(链表,元组等)中的所有数据进行下列操作:用传给reduce中的函数 func()(必须是 ...

  5. 2015-09-14-初级vector

    标准库vector类型 #include<vector> using std::vector; vector为一个类模板. vector的初始化 vector<T> v1; v ...

  6. jsPDF

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  7. [PyTorch入门]之数据导入与处理

    数据导入与处理 来自这里. 在解决任何机器学习问题时,都需要在处理数据上花费大量的努力.PyTorch提供了很多工具来简化数据加载,希望使代码更具可读性.在本教程中,我们将学习如何从繁琐的数据中加载. ...

  8. 方兴未艾的云计算:SoCC 2015大会

    ACM 云计算研讨会(ACM Symposium on Cloud Computing, 以下简称SoCC)是由SIGMOD(Special Interest Group on Management ...

  9. 《Java 8实战》读书笔记系列——第三部分:高效Java 8编程(四):使用新的日期时间API

    https://www.lilu.org.cn/https://www.lilu.org.cn/ 第十二章:新的日期时间API 在Java 8之前,我们常用的日期时间API是java.util.Dat ...

  10. 解决Request中参数中文乱码问题

    1.使用配置过滤器的方式解决 在web.xml中增加过滤器: <!--配置解决中文乱码的过滤器--> <filter> <filter-name>character ...