一、原子类

1、CAS算法

  强烈建议读者看这篇之前,先看这篇 初识JUC 的前两节,对原子性,原子变量,内存可见性有一个初步认识。

  CAS(Compare and Swap)是一种硬件对并发的支持,针对多处理器操作而设计的处理器中的一种特殊指令,用于管理对共享数据的并发访问,是硬件对于并发操作共享数据的支持。它是一个原子性的操作,对应到CPU指令为cmpxchg。它是一条CPU并发原语。
  CAS包含了3个操作数:内存值V,比较值A,更新值B。当且仅当V == A时,V = B,否则不执行任何操作。
  CAS算法:当多个线程并发的对主存中的数据进行修改的时候。有且只有一个线程会成功,其他的都会失败(同时操作,只是会失败而已,并不会被锁之类的)。
  CAS是一种无锁的非阻塞算法,是乐观锁的一种实现。不存在上下文切换的问题。
  CAS比普通同步锁效率高,原因:CAS算法当这一次不成功的时候,它下一次不会阻塞,也就是它不会放弃CPU的执行权,它可以立即再次尝试,再去更新。
  通俗的说:我要将变量 i 由 2 修改为 3。当内存中 i == 2,且修改成功,才为成功。若内存中 i 由于其他线程的操作已经不是 2 了,那此次我的修改视为失败。

2、简单使用

  JDK 1.5 以后java.util.concurrent.atomic包下提供了常用的原子变量。它支持单个变量上的无锁线程安全编程。这些原子变量具备以下特点:volatile的内存可见性;CAS算法保证数据的原子性。

  atomic包描述:图片来源API文档

  代码示例:原子变量使用

 1 public class Main {
2 public static void main(String[] args) {
3 AtomicInteger integer = new AtomicInteger(2);
4
5 boolean b = integer.compareAndSet(3, 5);
6 System.out.println(b);
7 System.out.println(integer.get());
8
9 b = integer.compareAndSet(2, 10);
10 System.out.println(b);
11 System.out.println(integer.get());
12
13 // 等价于 i++
14 integer.getAndIncrement();
15
16 // 等价于 ++i
17 integer.incrementAndGet();
18 }
19 }
20
21 // 结果
22 false
23 2
24 true
25 10

  分析:很简单,设置初始值为 2。
  ①由 3 修改成5,而设置初始值内存值为2,所以修改失败,返回false。
  ②由 2 修改成10,初始值内存值为2,所以修改成功,返回true。

3、源码分析

  这些原子变量底层就是通过CAS算法来保证数据的原子性。
  源码示例:AtomicInteger 类

 1 public class AtomicInteger extends Number implements java.io.Serializable {
2 private static final long serialVersionUID = 6214790243416807050L;
3
4 // setup to use Unsafe.compareAndSwapInt for updates
5 private static final Unsafe unsafe = Unsafe.getUnsafe();
6 private static final long valueOffset;
7
8 // 获取value在内存的地址偏移量
9 static {
10 try {
11 valueOffset = unsafe.objectFieldOffset
12 (AtomicInteger.class.getDeclaredField("value"));
13 } catch (Exception ex) { throw new Error(ex); }
14 }
15
16 private volatile int value;
17
18 public AtomicInteger(int initialValue) {
19 value = initialValue;
20 }
21
22 public AtomicInteger() {
23 }
24
25 public final int get() {
26 return value;
27 }
28
29 public final void set(int newValue) {
30 value = newValue;
31 }
32
33 public final boolean compareAndSet(int expect, int update) {
34 return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
35 }
36
37 public final int getAndIncrement() {
38 return unsafe.getAndAddInt(this, valueOffset, 1);
39 }
40
41 public final int incrementAndGet() {
42 return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
43 }
44
45 }

  说明:public final boolean compareAndSet(int expect, int update)
  变量valueOffset:通过静态代码块获取变量value在内存中的偏移地址。
  变量value:用volatile修饰,这里体现了"多线程之间的内存可见性"。
  this:即 AtomicInteger 对象本身。
  很容易理解:就是将当前对象 this 的变量value,由期望值 expect 修改为 update。

  源码示例:Unsafe 类

 1 public final class Unsafe {
2
3 public native void throwException(Throwable var1);
4
5 public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
6
7 public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
8
9 public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
10
11 public native int getIntVolatile(Object var1, long var2);
12
13
14 public final int getAndAddInt(Object var1, long var2, int var4) {
15 int var5;
16 do {
17 // 获取对象var1的变量var2的内存值
18 var5 = this.getIntVolatile(var1, var2);
19 } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
20
21 return var5;
22 }
23
24 }

  Unsafe是CAS的核心类,其所有方法都是native修饰的。也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务,是由C/C++编写的本地方法。CAS算法的实现,也是由Unsafe类通过调用本地方法直接操作特定内存数据来实现的。
  getAndIncrement()方法能够在多线程环境保证变量的原子性自增。但源码中,并没有加synchronized或者lock锁,那么,它是如何保证的呢?其实很简单:

  先获取一次变量的内存值,然后通过CAS算法进行比较更新。失败了就一直不停的重试,是一个循环的过程,这个过程也称作自旋。
  这就是为什么 AtomicInteger 的自增操作具备原子性。

1 private AtomicInteger i = new AtomicInteger();
2 public int getI() {
3 return i.getAndIncrement();
4 }

4、CAS的缺点

  (1)ABA问题。
  (2)循环时间变长:高并发情况下,使用CAS可能会存在一些线程一直循环修改不成功,导致循环时间变长,这会给CPU带来很大的执行开销。由于AtomicInteger中的变量是volatile的,为了保证内存可见性,需要保证缓存一致性,通过总线传输数据,当有大量的CAS循环时,会产生总线风暴。
  (3)只能保证一个变量的原子操作:如果需要保证多个变量操作的原子性,是做不到的。对于这种情况只能使用synchronized或者juc包中的Lock工具。

二、ABA问题

1、介绍

  代码示例:演示ABA问题

 1 // 原子引用类演示ABA问题
2 public class ABATest {
3 public static void main(String[] args) throws InterruptedException {
4 AtomicReference<String> reference = new AtomicReference<>("A");
5
6 // 线程 t1 由A修改B,又由B修改A
7 new Thread(() -> {
8 System.out.println(reference.compareAndSet("A", "B") + ". " + Thread.currentThread().getName() + " value is:" + reference.get());
9 System.out.println(reference.compareAndSet("B", "A") + ". " + Thread.currentThread().getName() + " value is:" + reference.get());
10 }, "t1").start();
11
12
13 new Thread(() -> {
14 // 让t1线程完成ABA操作
15 try {
16 Thread.sleep(500);
17 } catch (InterruptedException e) {
18 e.printStackTrace();
19 }
20 System.out.println(reference.compareAndSet("A", "C") + ". " + Thread.currentThread().getName() + " value is:" + reference.get());
21
22 }, "t2").start();
23
24 Thread.sleep(1000);
25
26 System.out.println(reference.get());
27 }
28 }
29
30 // 结果
31 true. t1 value is:B
32 true. t1 value is:A
33 true. t2 value is:C
34 C

  如何理解ABA问题?
  可能你会觉得,线程 t2 不就是要将"A"改为"C"嘛,虽然中间变化了,但对 t2 也没影响呀!
  比如:你的银行卡里有10w,中间你领了工资1w,然后,又被扣除还了房贷1w,此时,你的银行卡里还是10w。虽然结果没变,但余额已经不是原来的余额了。而且,你一定在意中间你的钱去哪里了,所以是不一样的。
  再比如:对于公司财务来说,可能某一时刻,账户是100w,你偷偷挪用了公款20w,后来又悄悄补上了。虽然结果没变,但中间的记账明细,其实我们是关心的,因为这个时候你已经犯法了。

2、解决

  带时间戳的原子引用:Java提供了AtomicStampedReference来解决ABA问题。其实其实就是加了版本号,每一次的修改,版本号都 +1。比对的是 内存值 + 版本号 是否一致。
  代码示例:解决ABA问题

 1 public class ABATest {
2 public static void main(String[] args) throws InterruptedException {
3
4 AtomicStampedReference<String> reference = new AtomicStampedReference<>("A", 1);
5 final int stamp = reference.getStamp();
6
7 // 线程 t1 由A修改B,又由B修改A
8 new Thread(() -> {
9 System.out.println(reference.compareAndSet("A", "B", stamp, stamp + 1) + ". " + Thread.currentThread().getName() + " value is:" + reference.getReference());
10 System.out.println(reference.compareAndSet("B", "A", reference.getStamp(), reference.getStamp() + 1) + ". " + Thread.currentThread().getName() + " value is:" + reference.getReference());
11 }, "t1").start();
12
13
14 new Thread(() -> {
15 // 让t1线程完成ABA操作
16 try {
17 Thread.sleep(500);
18 } catch (InterruptedException e) {
19 e.printStackTrace();
20 }
21 System.out.println(reference.compareAndSet("A", "C", stamp, stamp + 1) + ". " + Thread.currentThread().getName() + " value is:" + reference.getReference());
22
23 }, "t2").start();
24
25 Thread.sleep(1000);
26
27 System.out.println(reference.getReference());
28 }
29 }
30
31 // 结果
32 true. t1 value is:B
33 true. t1 value is:A
34 false. t2 value is:A // t2并没有修改成功
35 A

  compareAndSet()方法的 4 个参数:

  expectedReference:表示期望的引用值
  newReference:表示要修改后的新引用值
  expectedStamp:表示期望的戳(版本号)
  newStamp:表示修改后新的戳(版本号)

3、源码分析

 1 public class AtomicStampedReference<V> {
2
3 private static class Pair<T> {
4 final T reference;
5 final int stamp;
6 private Pair(T reference, int stamp) {
7 this.reference = reference;
8 this.stamp = stamp;
9 }
10 static <T> Pair<T> of(T reference, int stamp) {
11 return new Pair<T>(reference, stamp);
12 }
13 }
14
15 public boolean compareAndSet(V expectedReference,
16 V newReference,
17 int expectedStamp,
18 int newStamp) {
19 Pair<V> current = pair;
20 return
21 expectedReference == current.reference &&
22 expectedStamp == current.stamp &&
23 ((newReference == current.reference &&
24 newStamp == current.stamp) ||
25 casPair(current, Pair.of(newReference, newStamp)));
26 }
27
28 private boolean casPair(Pair<V> cmp, Pair<V> val) {
29 return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
30 }
31 }

  很简单,维护了一对Pair,里面除了引用reference,还有一个int类型的戳(版本号)。比较更新的时候,两个变量都要比较。

三、LongAdder

1、介绍

  《阿里巴巴Java开发手册》推荐使用LongAdder。

  AtomicLong,本质上是多个线程同时操作同一个目标资源,有且只有一个线程执行成功,其他线程都会失败,不断重试(自旋),自旋会成为瓶颈。
  而LongAdder的思想就是把要操作的目标资源[分散]到数组Cell中,每个线程对自己的Cell变量的value进行原子操作,大大降低了失败的次数。
  这就是为什么在高并发场景下,推荐使用LongAdder的原因。

  参考文档:https://www.matools.com/api/java8
  《阿里巴巴Java开发手册》百度网盘:https://pan.baidu.com/s/1aWT3v7Efq6wU3GgHOqm-CA 密码: uxm8

聊聊并发(六)——CAS算法的更多相关文章

  1. 并发策略-CAS算法

    对于并发控制而言,我们平时用的锁(synchronized,Lock)是一种悲观的策略.它总是假设每一次临界区操作会产生冲突,因此,必须对每次操作都小心翼翼.如果多个线程同时访问临界区资源,就宁可牺牲 ...

  2. 聊聊并发(六)——ConcurrentLinkedQueue的实现原理分析

    1. 引言 在并发编程中我们有时候需要使用线程安全的队列.如果我们要实现一个线程安全的队列有两种实现方式:一种是使用阻塞算法,另一种是使用非阻塞算法.使用阻塞算法的队列可以用一个锁(入队和出队用同一把 ...

  3. (转载)java高并发:CAS无锁原理及广泛应用

    java高并发:CAS无锁原理及广泛应用   版权声明:本文为博主原创文章,未经博主允许不得转载,转载请注明出处. 博主博客地址是 http://blog.csdn.net/liubenlong007 ...

  4. CAS 算法与 Java 原子类

    乐观锁 一般而言,在并发情况下我们必须通过一定的手段来保证数据的准确性,如果没有做好并发控制,就可能导致脏读.幻读和不可重复度等一系列问题.乐观锁是人们为了应付并发问题而提出的一种思想,具体的实现则有 ...

  5. 聊聊并发(一)——初始JUC

    一.volatile 1.介绍 JDK 5.0 提供了java.util.concurrent包,在此包中增加了并发编程中很常用的使用工具类,用于定义类似于线程的自定义子系统,包括线程池.异步IO和轻 ...

  6. 三、原子变量与CAS算法

    原子变量:jdk1.5 后 java.util.concurrent.atomic 包下提供了常用的原子变量: - AtomicBoolean - AtomicInteger - AtomicLong ...

  7. Java多线程系列——原子类的实现(CAS算法)

    1.什么是CAS? CAS:Compare and Swap,即比较再交换. jdk5增加了并发包java.util.concurrent.*,其下面的类使用CAS算法实现了区别于synchronou ...

  8. 原子变量与CAS算法(二)

    一.锁机制存在的问题 (1)在多线程竞争下,加锁.释放锁会导致比较多的上下文切换和调度延时,引起性能问题. (2)一个线程持有锁会导致其它所有需要此锁的线程挂起. (3)如果一个优先级高的线程等待一个 ...

  9. (一)juc线程高级特性——volatile / CAS算法 / ConcurrentHashMap

    1. volatile 关键字与内存可见性 原文地址: https://www.cnblogs.com/zjfjava/category/979088.html 内存可见性(Memory Visibi ...

随机推荐

  1. .NET下使用ufun函数取CAM操作的进给速度

    UF_PARAM_ask_subobj_ptr_value,这个函数在封装的时候,给了很大一个坑啊. NXOpen.UF.UFParam.AskSubobjPtrValue(ByVal param_t ...

  2. UltraSoft - Beta - Scrum Meeting 7

    Date: May 23rd, 2020. Scrum 情况汇报 进度情况 组员 负责 今日进度 q2l PM.后端 暂无 Liuzh 前端 编写忘记密码界面 Kkkk 前端 暂无 王fuji 前端 ...

  3. Noip模拟50 2021.9.10

    已经好长时间没有考试不挂分的良好体验了... T1 第零题 开场数据结构,真爽 对于这道题首先要理解对于一条链从上向下和从下向上走复活次数相等 (这可能需要晚上躺在被窝里面脑摸几种情况的样例) 然后就 ...

  4. allegro查看线宽的方法

  5. 算法:N-gram语法

    一.N-gram介绍 n元语法(英语:N-gram)指文本中连续出现的n个语词.n元语法模型是基于(n - 1)阶马尔可夫链的一种概率语言模型,通过n个语词出现的概率来推断语句的结构.这一模型被广泛应 ...

  6. linux 内核源代码情景分析——几个重要的数据结构和函数

    页面目录PGD.中间目录PMD和页面表PT分别是由表项pgd_t.pmd_t和pte_t构成的数组,而这些表项都是数据结构 1 /* 2 * These are used to make use of ...

  7. vue打包后反编译到源代码(reverse-sourcemap)

    因为突然的疫情把我困在家了,家里的电脑没有源代码,但是需求还要改,工作还得继续... 从服务器下载了之前上传的打包后的文件,找了一圈反编译方法,得救了,在此记录一下. 1.npm install -- ...

  8. cesium制作自己的骑行轨迹

    制作自己的骑行轨迹 马上国庆节了,计划骑车回家,突然想到把所有的骑行线路汇总一下,无奈码表和APP不支持这样的操作,出于职业病,在此操作一下. 我用的是黑鸟码表,可以导出fit运动轨迹,但是fit还需 ...

  9. java 垃圾回收及内存分配策略

    一.在垃圾收集器对堆进行回收前,首先需要判断对象是否"存活",对已经"死去"的对象进行回收 判断对象是否存活:引用计数法和可达性分析法 引用计数法:给对象添加一 ...

  10. ESXi 6.7 的https服务挂掉处理方法 503 Service Unavailable

    首先进入EXSi开启SSH(ESXi的主机控制台,非web控制台,是安装esxi的控制台) 然后 /etc/init.d/hostd status 显示已停止, 使用 /etc/init.d/host ...