Java线程四之CAS和原子类
一、CAS
1. 问题引入
- 假设现银行账户中存在10000元,现在有1000个线程同时来操作这些余额,每个线程要减少10元,那么,当1000个线程运行完成后,账户中余额会是预想中的0元吗?根据之前所学习的知识可以知道,显然不会,因为多个线程操作共享变量必然会存在线程安全问题。
- 我们应该如何去解决线程安全问题?① 方法一(不举例):加synchronized锁,让同一时刻只能有一个线程去操作共享变量。② 方法二(如下代码):采用Java中提供的原子类AtomicInteger来存余额,利用
boolean compareAndSet(int expect, int update)
方法来进行余额的更改,该方法传入两个参数分别为为更改前的原值和更改后的目标值,返回值为是否更改成功,如果更改成功则退出循环,否则无限循环进行更改操作。//方法二 class AccountSafe implements Account { private AtomicInteger balance; public AccountSafe(Integer balance) { this.balance = new AtomicInteger(balance); } @Override public Integer getBalance() { return balance.get(); } @Override public void withdraw(Integer amount) { while (true) { int prev = balance.get(); int next = prev - amount; //比较并设置 if (balance.compareAndSet(prev, next)) { break; } } } }
2. 方法解析
上述的方法二的关键就是compareAndSet,它的简称就是CAS,为什么该方法可以保证线程安全呢?
① compareAndSet方法在CPU的底层实现中使用了lock cmpxchg指令(X86架构),保证了该方法执行的原子性,即该方法的运行过程不会被其他线程所打断。
② compareAndSet方法是对原子类中的volatile关键字修饰的变量进行的更改,直接对主存中的数据进行了更改,利用读写屏障保证了该变量的可见性并维持了代码执行的有序性。CAS必须借助volatile才能读到共享变量的最新值。
3. CAS特点
- CAS的方式进行解决时,即使重试失败,线程始终在高速运行,没有停歇。它不会像synchronized一样让线程在没有获得锁的时候发生上下文切换进入阻塞。因此,CAS方式的效率要高一些。
- CAS方式是基于乐观锁的思想实现的,它不怕别的线程来修改共享变量,会在每次更新值的时候进行比较来决定是否更新。
- CAS方式体现的是无锁并发、无阻塞并发。无锁即不加锁,无阻塞即不等待。
- 注意:CAS方式需要额外的CPU支持,因为CAS方式的自旋需要不断地在CPU上运行,因此CAS方式适用于线程数少、多核CPU的场景。
4. ABA问题
4.1 基本概念
- 线程A利用CAS对某个变量i进行修改之前,其他线程将i的值改完之后又改回去了,此时线程A无法察觉到i的值已经发生过变化,依然会对变量i按照原定的方式进行修改。
4.2 解决方式
- 如果是自己定义的CAS自旋锁,则可以加版本号或时间戳。
- 如果是使用的原子类,则可以使用更加安全的其他原子类。
二、Java中的原子类
1. 原子整数
1.1 AtomicInteger
方法 | 作用 |
---|---|
get() | 得到当前值 |
incrementAndGet() | 先加1后获取 |
getAndIncrement() | 先获取后加1 |
getAndAdd(int num) | 先加num后获取 |
addAndGet(int num) | 先获取后加num |
updateAndGet(IntUnaryOperator updateFunction) | 传入参数为函数式接口,先按照传入的规则进行更新再获取 |
getAndUpdate(IntUnaryOperator updateFunction) | 传入参数为函数式接口,先获取再按照传入的规则进行更新 |
1.2 AtomicLong
1.3 AtomicBoolean
2. 原子引用
2.1 AtomicReference
- 原子整数类只能用于保障基本数据类型,原子引用类就用于保障引用数据类型。
2.2 AtomicStampedReference
- 与AtomicReference相比,AtomicStampedReference增加了一个版本号,解决了ABA的问题。
2.3 AtomicMarkableReference
- 我们并不关心其他线程对引用变量更改了几次,只关心是否有线程对变量进行了更改,因此定义了了新的原子引用类。它采用一个布尔值来代替版本号,布尔值用来记录变量的更改情况。
3. 原子数组
原子引用类只能对整个引用进行保护,不能保护引用中的值,因此,定义了原子数组类,对数组中每个元素都进行保护。
3.1 AtomicReferenceArray
3.2 AtomicIntegerArray
3.3 AtomicLongArray
4. 字段更新器
字段更新器,主要是用于针对对象的某个属性进行原子操作,保障线程安全,要求该对象的属性必须为volatile的。
4.1 AtomicReferenceFieldUpdater
4.2 AtomicIntegerFieldUpdater
4.3 AtomicLongFieldUpdater
5. 原子累加器
专门用于进行累加的构造器,其效率要高于其他原子类自己的累加操作。
5.1 LongAdder
5.1.1 性能提升原因
- 性能提升的原因很简单,就是在有竞争时,设置多个累加单元,Therad-0累加Cell[0],而Thread-1累加Cell[1]…,最后将结果汇总。这样它们在累加时操作的不同的Cell变量,因此减少了CAS重试失败,从而提高性能。注意,Cell变量的个数一定是小于等于CPU核数的。
5.2.2 源码分析
-
关键域
//累加单元数组, 懒惰初始化 transient volatile Cell[] cells; //基础值, 如果没有竞争, 则用cas累加这个域 transient volatile long base; //在cells创建或扩容时, 置为1, 表示加锁 transient volatile int cellsBusy;
-
CAS锁
public class LockCas { private AtomicInteger state = new AtomicInteger(0); public void lock() { while (true) { if (state.compareAndSet(0, 1)) { break; } } } public void unlock() { log.debug("unlock..."); state.set(0); } }
-
Cell累加单元
// 防止缓存行伪共享 @sun.misc.Contended static final class Cell { volatile long value; Cell(long x) { value = x; } // 最重要的方法, 用来cas方式进行累加, prev表示旧值, next表示新值 final boolean cas(long prev, long next) { return UNSAFE.compareAndSwapLong(this, valueOffset, prev, next); } // 省略不重要代码 }
-
add()方法
public void add(long x) { // as 为累加单元数组 // b 为基础值 // x 为累加值 Cell[] as; long b, v; int m; Cell a; // 进入 if 的两个条件 // 1. as 有值, 表示已经发生过竞争, 进入 if // 2. cas 给 base 累加时失败了, 表示 base 发生了竞争, 进入 if if ((as = cells) != null || !casBase(b = base, b + x)) { // uncontended 表示 cell 没有竞争 boolean uncontended = true; if ( // as 还没有创建 as == null || (m = as.length - 1) < 0 || // 当前线程对应的 cell 还没有 (a = as[getProbe() & m]) == null || // cas 给当前线程的 cell 累加失败 uncontended=false ( a 为当前线程的 cell ) !(uncontended = a.cas(v = a.value, v + x)) ) { // 进入 cell 数组创建、cell 创建的流程 longAccumulate(x, null, uncontended); } } }
1.如果第一个线程到来,此时不存在竞争,所以cells数组必定为null,此时进行casBase操作必定能成功,此时无需进入代码块。
2.如果第一个线程进行caseBase操作时,第二个线程进入,此时cells依然为null,没有开始竞争,所以,第二个线程企图进行casBase操作,但是由于此时第一个线程正在进行该操作,所以第二个线程进入if的代码块内,此时cells仍然为null,执行longAccmulate方法。
3.接下来,第三个线程到来,此时cells数组不为null,说明已经存在了竞争,因此,直接进入代码块,此时cells不为null,则判断当前线程是否存在一个对应的cells内的累加单元cell,如果存在了,直接进行累加,累加成功则直接返回,如果不成功则执行longAccmulate方法。如果不存在,直接执行longAccmulate方法。 -
longAccumulate()方法
① else if (cellsBusy == 0 && cells == as && casCellsBusy()) ==> 数组不存在时
② if ((as = cells) != null && (n = as.length) > 0) ==> Cells数组存在但是当前线程对应的累加单元cell不存在时
③ 接②成功的情况走下面的esle if
final void longAccumulate(long x, LongBinaryOperator fn, boolean wasUncontended) { int h; // 当前线程还没有对应的 cell, 需要随机生成一个 h 值用来将当前线程绑定到 cell if ((h = getProbe()) == 0) { // 初始化 probe ThreadLocalRandom.current(); // h 对应新的 probe 值, 用来对应 cell h = getProbe(); wasUncontended = true; } // collide 为 true 表示需要扩容 boolean collide = false; for (;;) { Cell[] as; Cell a; int n; long v; // 已经有了 cells if ((as = cells) != null && (n = as.length) > 0) { // 还没有 cell if ((a = as[(n - 1) & h]) == null) { // 为 cellsBusy 加锁, 创建 cell, cell 的初始累加值为 x // 成功则 break, 否则继续 continue 循环 } // 有竞争, 改变线程对应的 cell 来重试 cas else if (!wasUncontended) wasUncontended = true; // cas 尝试累加, fn 配合 LongAccumulator 不为 null, 配合 LongAdder 为 null else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x)))) break; // 如果 cells 长度已经超过了最大长度(cpu个数), 或者已经扩容, 改变线程对应的 cell 来重试 cas else if (n >= NCPU || cells != as) collide = false; // 确保 collide 为 false 进入此分支, 就不会进入下面的 else if 进行扩容了 else if (!collide) collide = true; // 加锁 else if (cellsBusy == 0 && casCellsBusy()) { // 加锁成功, 扩容 continue; } // 改变线程对应的 cell,换个累加单元累加 h = advanceProbe(h); } // 还没有 cells, 尝试给 cellsBusy 加锁 else if (cellsBusy == 0 && cells == as && casCellsBusy()) { // 加锁成功, 初始化 cells, 最开始长度为 2, 并填充一个 cell // 成功则 break; } // 上两种情况失败, 尝试给 base 累加 else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x)))) break; } }
-
sum()方法
public long sum() { Cell[] as = cells; Cell a; long sum = base; if (as != null) { for (int i = 0; i < as.length; ++i) { if ((a = as[i]) != null) sum += a.value; } } return sum; }
5.3.3 原理解析
1.Cell的缓存伪共享
- 现代CPU的结构:现代CPU一般都采用三级缓存的结构,CPU从不同的位置中读取数据的速度是不同的,到寄存器大约需要的时钟周期为1cycle,到一级缓存需要3到4 cycle,到二级缓存需要10到20cycle,到三级缓存需要40到45cycle,到内存需要120~240cycle。因为CPU与内存的速度差异很大,所以需要靠预读数据至缓存来提升效率。
- 缓存行:
① 缓存以缓存行为单位,每个缓存行对应着一块内存,一般是64byte(8个long)。缓存的加入会造成数据副本的产生,即同一份数据会缓存在不同核心的缓存行中。使用volatile关键字修饰数据后,CPU要保证数据的一致性,也就是说如果某个CPU核心更改了数据,其它CPU核心对应的整个缓存行必须失效,同时该CPU会将更新的数据同步到内存中,其他数据如果使用这些数据则必须要到内存中去取。
② 因为Cell是数组形式,在内存中是连续存储的(伪共享),一个Cell为24字节(16字节的对象头和8字节的 value),因此缓存行可以存下2个Cell对象。如下图所示,Core-0要修改Cell[0],Core-1要修改Cell[1]。无论谁修改成功,都会导致对方Core的缓存行失效。
@sun.misc.Contended
用来解决这个问题,它的原理是在使用此注解的对象或字段的前后各增加128字节大小的padding,从而让CPU将对象预读至缓存时占用不同的缓存行,这样,不会造成更新Cell[0]时让对方Cell[1]缓存行的失效。
5.2 LongAccumulator
三、Unsafe对象(了解)
- 基本概念:Unsafe对象提供了非常底层的,操作内存、线程的方法,Unsafe对象不能直接调用,只能通过反射获得。