深入学习掌握JUC并发编程系列(五) -- 深入浅出无锁-乐观锁
一、CAS(原子性)与volatile(可见性)
public void withdraw(Integer amount) {
// 需要不断尝试,直到成功为止
while (true) {
// 比如拿到了旧值 100
int prev = balance.get();
// 在这个基础上 100-10 = 90
int next = prev - amount;
/*
compareAndSet 正是做这个检查,在 set 前,
先比较 prev与最新值
- 不一致了,next作废,返回 false 表示失败
比如,别的线程已经做了减法,当前值已经被减成了90
那么本线程的这次 90就作废了,进入下次循环重试
- 一致,以 next 设置为新值,返回 true 表示成功
*/
if (balance.compareAndSet(prev, next)) {
break;
}
}
}
- CAS(compareAndSet/swap):比较并设置/交换,比较prev和读取共享变量的最新值是否相等:
- 相等:返回True,并将next作为共享变量的新值写入内存中
- 不相等:返回False,继续重试
- CAS 底层: lock cmpxchg 指令(X86 架构),在单核 /多核CPU 下都能够保证比较-交换的原子性
- CAS需要volatile的支持,读取到共享变量的最新值(可见性),这样才能实现比较并交换的效果
- 效率高:CAS即使比较失败,线程也在运行;而synchronized会让竞争失败的线程停止运行(阻塞)
- 前提条件:多核CPU,且核心数大于线程数(当CAS比较失败时, 不会因为没有分到时间片导致线程上下文切换,让线程进入可运行状态)
- 特点:CAS+Volatile
- 无锁:未使用synchronized给线程加锁,实现无锁并发
- 无阻塞(效率高):未使用synchronized阻塞线程,使用while(True)不断重试,实现无阻塞并发
- 乐观锁:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,可以继续重试
- 悲观锁(synchronized):最悲观的估计,得防着其它线程来修改共享变量,我上了锁你们都别想改,我改完了解开锁,你们才有机会
二、原子整数
- JUC并发包提供了:AtomicBoolean、AtomicInteger、AtomicLong
- AtomicInteger示例:compareAndSet是以下方法的实现基础
AtomicInteger i = new AtomicInteger(0);
// 获取并自增(i = 0, 结果 i = 1, 返回 0),类似于 i++
i.getAndIncrement()
// 自增并获取(i = 1, 结果 i = 2, 返回 2),类似于 ++i
i.incrementAndGet()
// 自减并获取(i = 2, 结果 i = 1, 返回 1),类似于 --i
i.decrementAndGet()
// 获取并自减(i = 1, 结果 i = 0, 返回 1),类似于 i--
i.getAndDecrement()
// 获取并增加值(i = 0, 结果 i = 5, 返回 0)
i.getAndAdd(5)
// 增加值并获取(i = 5, 结果 i = 2, 返回 2)
i.addAndGet(-3)
// 获取并更新(i = 2, x 为 i 的当前值, 结果 i = 10, 返回 10)
// 参数为函数式接口,可以使用lambda表达式
i.getAndUpdate(x -> x*5)
// 更新并获取(i = 10, p 为 i 的当前值, 结果 i = 0, 返回 0)
i.updateAndGet(p -> p - 10)
// 获取并计算(i = 0, p 为 i 的当前值, x 为第1个参数10, 结果 i = 10, 返回 0)
i.getAndAccumulate(10, (p, x) -> p + x)
// 计算并获取(i = 10, p 为 i 的当前值, x 为第1个参数10, 结果 i = 0, 返回 0)
i.accumulateAndGet(-10, (p, x) -> p + x)
三、原子引用(Reference)
- AtomicReference、AtomicMarkableReference 、AtomicStampedReference (String属于引用)
- 以AtomicReference为例:范型
private AtomicReference<BigDecimal> balance;
public DecimalAccountCas(BigDecimal balance) {
this.balance = new AtomicReference<>(balance);
}
- ABA问题:
- 问题:CAS无法知道初值A中间被其它线程修改过(A-B-A),仅能判断出共享变量的最新值与最初值 A 是否相同,不能感知到从A 改为B 又改回A 的情况
- 解决:如果主线程希望,只要有其它线程修改过共享变量,那么CAS就算失败, 需要再加一个版本号(使用 AtomicStampedReference)
- AtomicStampedReference:
- 两个参数(引用变量值,版本号 int)
- 作用:给原子引用加上版本号,追踪原子引用整个的变化过程
static AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
public static void main(String[] args) throws InterruptedException {
// 获取值 A
String prev = ref.getReference();
// 获取版本号
int stamp = ref.getStamp();
- AtomicMarkableReference:
- 两个参数(引用变量值,标记 boolean)
- 作用:给原子引用加上标记,只关心原子引用是否更改过,不关心更改了几次
四、原子数组(Array)
- AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
- 通过函数式接口,编写测试方法
/**
参数1,提供数组、可以是线程不安全数组或线程安全数组
参数2,获取数组长度的方法
参数3,自增方法,回传 array, index
参数4,打印数组的方法
*/
// supplier 提供者 无中生有 ()->结果
// function 函数 一个参数一个结果 (参数)->结果 , BiFunction (参数1,参数2)->结果
// consumer 消费者 一个参数没结果 (参数)->void, BiConsumer (参数1,参数2)->
private static <T> void demo(
Supplier<T> arraySupplier,
Function<T, Integer> lengthFun,
BiConsumer<T, Integer> putConsumer,
Consumer<T> printConsumer ) {
List<Thread> ts = new ArrayList<>();
T array = arraySupplier.get(); //Supplier
int length = lengthFun.apply(array); //Function
for (int i = 0; i < length; i++) {
// 每个线程对数组作 10000 次操作
ts.add(new Thread(() -> {
for (int j = 0; j < 10000; j++) {
putConsumer.accept(array, j%length); //BiConsumer
}
}));
}
printConsumer.accept(array); //Consumer
}
// lambda表达式,调用测试方法(原子数组)
demo(
()-> new AtomicIntegerArray(10),
(array) -> array.length(),
(array, index) -> array.getAndIncrement(index),
array -> System.out.println(array)
);
五、字段更新器(Filed)
- AtomicReferenceFieldUpdater、AtomicIntegerFieldUpdater、AtomicLongFieldUpdater
- 保护对象的某个(Field)域/属性/字段/成员变量的原子性
- 字段必须使用 volatile 修饰,否则会出现异常 Exception in thread “main” java.lang.IllegalArgumentException: Must be volatile type
public class Student {
private volatile String name;
public static void main(String[] args) {
AtomicReferenceFieldUpdater fieldUpdater =AtomicReferenceFieldUpdater.newUpdater(Student.class, Sting.class, "name");
}
}
六、原子累加器
- LongAdder、LongAccumulator、DoubleAdder、DoubleAccumulator
- LongAdder 比 AtomicLong的 getAndIncrement()方法累加性能更好
- 性能提升原因:
- AtomicLong:在有竞争时,不断 CAS 重试
- LongAdder:在有竞争时,设置多个累加单元,Therad-0累加 Cell[0], Thread-1累加 Cell[1],最后再将结果汇总
- 在累加时不同线程操作不同的 Cell 变量,减少了CAS 重试失败次数,从而提高性能
七、LongAdder源码分析
1. LongAdder 类的几个关键字段
- cells 累加单元、base 基础值、cellsBusy 是否加锁(cas加锁)
- transient(不被序列化)、volatie(可见性)
// 累加单元数组, 懒惰初始化(有竞争时)
transient volatile Cell[] cells;
// 基础值, 如果没有竞争, 则用 cas 累加这个域
transient volatile long base;
// 在 cells 创建或扩容时, 置为 1, 表示加锁(使用cas加锁)
transient volatile int cellsBusy;
2. 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);
}
}
3. Cell累加单元类
懒惰创建,有竞争时才创建
// Contended(竞争)注解:防止缓存行伪共享
@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);
}
}
4. 缓存行伪共享
- 缓存行原理:
- CPU 与 内存的速度差异很大,需要将数据提前读至缓存来提升效率
- 缓存以缓存行为单位,每个缓存行对应着一块内存,64 bytes(8 个 long)
- 缓存行弊端:
- 产生数据副本(同一份数据会缓存在不同核心的缓存行中)
- 保证数据的一致性:同一份数据,如果某个 CPU 核心更改了数据,其它 CPU 核心对应的整个缓存行必须失效
- 缓存行伪共享问题:
- 一个缓存行可以存下 2 个 Cell 对象:Cell是数组形式(在内存中连续存储),一个 Cell 24 字节(16 字节的对象头和 8 字节的 value)
- Core-0 修改Cell[0],Core-1 修改Cell[1],无论谁修改成功,都会导致对方 Core 的缓存行失效
- 问题解决:@sun.misc.Contended(注解)
- 在对象或字段的前后各增加 128 字节大小的padding(空白)
- CPU 将Cell累加单元对象预读至缓存时,占用不同的缓存行,不会造成对方缓存行的失效
5. add() 源码
public void add(long x) {
// as 为累加单元数组
// b 为基础值(旧值)
// x 为累加值
// a 为累加值当前现场已创建的累加单元
Cell[] as; long b, v; int m; Cell a;
// 进入 下一个if 的两个条件
// 1. as 有值, 表示已经发生过竞争, 进入 if
// 2. base cas累加时失败, 表示发生了竞争, 进入 if
if ((as = cells) != null || !casBase(b = base, b + x)) {
// uncontended 表示 cell 没有竞争
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[getProbe() & m]) == null || // 当前线程的cell(a)是否创建
!(uncontended = a.cas(v = a.value, v + x))) // 当前线程的cell(a)执行cas累加操作
longAccumulate(x, null, uncontended); // 进入 cell 数组创建、cell 创建的流程
}
}
6. longAccumulate源码 (创建cells数组)
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;
if ((as = cells) != null && (n = as.length) > 0) {
// a:累加单元是否创建
if ((a = as[(n - 1) & h]) == null) { // cells数组已创建,cell累加单元未创建:第二张图
// cellsBusy 加锁, 创建 cell累加单元, 进行累加
// 将cell累加单元放入cells数组,成功(槽位为空)则 break退出循环, 失败则继续循环
}
else if (!wasUncontended)
wasUncontended = true;
// 累加单元a 尝试 cas累加(v + x), 累加成功,则break退出循环
else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x)))) //cells数组已创建,cell累加单元已创建:第三张图
break;
// cas累加失败:判断 cells 数组长度n是否超过了cpu个数
else if (n >= NCPU || cells != as)
collide = false;
// 超过cpu上限,则collide 为 false 下次循环会进入此分支, 就不会进入下面的 else if 进行扩容了
else if (!collide)
collide = true;
// 判断是否加锁,并尝试加锁,失败则改变现场对应的cell累加单元
else if (cellsBusy == 0 && casCellsBusy()) {
// 加锁成功, 进行数组扩容,扩容后继续循环
continue;
}
// 改变线程对应的 cell累加单元,重新尝试cas累加
h = advanceProbe(h);
}
// cellsBusy=0 : 判断是否加锁 , cells=as : 判断是否有其它线程改变数组 , casCellsBusy() : 尝试将cellsBusy改为1 , 加锁)
else if (cellsBusy == 0 && cells == as && casCellsBusy()) { // as为null (cells数组不存在 ) : 第一张图
// 加锁成功, 创建 cells数组, 最开始长度为 2, 并初始化一个 cell累加单元,进行累加
// 解锁后, break退出循环;
}
// 加锁失败, 尝试给 base 累加 , 累加失败重新循环,累加成功break退出循环
else if (casBase(v = base, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
break;
}
}
7. 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;
}
8. unsafe对象
public class UnsafeAccessor {
static Unsafe unsafe;
static {
try {
// 通过反射获取unsafe对象
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe"); // getDeclaredField:获取私有属性: the Unsafe
theUnsafe.setAccessible(true); // 允许访问私有属性
unsafe = (Unsafe) theUnsafe.get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new Error(e);
}
}
static Unsafe getUnsafe() {
return unsafe;
}
}
- cas、pack/unpack底层都调用了 unsafe对象
- unsafe 对象提供了非常底层的,操作内存、线程的方法(不建议直接使用)
- unsafe 对象不能直接调用,只能通过反射获得
- unsafe实现cas操作:
- 获取域/字段/属性的偏移地址:unsafe.objectFieldOffset()
- 执行cas操作:unsafe.compareAndSwapInt()
- unsafe 模拟实现原子整数
class MyAtomicInteger {
private volatile int value;
private static final long valueOffset;
private static final Unsafe unsafe;
static {
unsafe = UnsafeAccessor.getUnsafe(); // 通过之前写好的方法,获取unsafe对象
try {
valueOffset = unsafe.objectFieldOffset(MyAtomicInteger.class.getDeclaredField("value"));
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
}
总结
- CAS与volatile(无锁并发)
- 重要API:原子整数、原子引用、原子数组、字段更新器、原子累加器
- unsafe 对象(底层)
- 原理:
- LongAdder源码
- 缓存行伪共享(cells累加单元数组,加入空隙,存储在不同缓存行)