深入学习掌握JUC并发编程系列(五) -- 深入浅出无锁-乐观锁
- 一、CAS(原子性)与volatile(可见性)
- 二、原子整数
- 三、原子引用(Reference)
- 四、原子数组(Array)
- 五、字段更新器(Filed)
- 六、原子累加器
- 七、LongAdder源码分析
-
- 1. LongAdder 类的几个关键字段
- 2. cellsBusy使用cas方式加锁原理:(不要用于实践!)
- 3. Cell累加单元类
- 4. 缓存行伪共享
- 5. add() 源码
- 6. longAccumulate源码 (创建cells数组)
- 7. sum 方法(统计累加结果)
- 8. unsafe对象
- 总结
一、CAS(原子性)与volatile(可见性)
public void withdraw(Integer amount) {
while (true) {
int prev = balance.get();
int next = prev - amount;
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.getAndIncrement()
i.incrementAndGet()
i.decrementAndGet()
i.getAndDecrement()
i.getAndAdd(5)
i.addAndGet(-3)
i.getAndUpdate(x -> x*5)
i.updateAndGet(p -> p - 10)
i.getAndAccumulate(10, (p, x) -> p + x)
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 {
String prev = ref.getReference();
int stamp = ref.getStamp();
- AtomicMarkableReference:
- 两个参数(引用变量值,标记 boolean)
- 作用:给原子引用加上标记,只关心原子引用是否更改过,不关心更改了几次
四、原子数组(Array)
- AtomicIntegerArray、AtomicLongArray、AtomicReferenceArray
- 通过函数式接口,编写测试方法
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();
int length = lengthFun.apply(array);
for (int i = 0; i < length; i++) {
ts.add(new Thread(() -> {
for (int j = 0; j < 10000; j++) {
putConsumer.accept(array, j%length);
}
}));
}
printConsumer.accept(array);
}
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;
transient volatile long base;
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累加单元类
懒惰创建,有竞争时才创建
@sun.misc.Contended
static final class Cell {
volatile long value;
Cell(long x) { value = x; }
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) {
Cell[] as; long b, v; int m; Cell a;
if ((as = cells) != null || !casBase(b = base, b + x)) {
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[getProbe() & m]) == null ||
!(uncontended = a.cas(v = a.value, v + x)))
longAccumulate(x, null, uncontended);
}
}
6. longAccumulate源码 (创建cells数组)
final void longAccumulate(long x, LongBinaryOperator fn,boolean wasUncontended) {
int h;
if ((h = getProbe()) == 0) {
ThreadLocalRandom.current();
h = getProbe();
wasUncontended = true;
}
boolean collide = false;
for (;;) {
Cell[] as; Cell a; int n; long v;
if ((as = cells) != null && (n = as.length) > 0) {
if ((a = as[(n - 1) & h]) == null) {
}
else if (!wasUncontended)
wasUncontended = true;
else if (a.cas(v = a.value, ((fn == null) ? v + x : fn.applyAsLong(v, x))))
break;
else if (n >= NCPU || cells != as)
collide = false;
else if (!collide)
collide = true;
else if (cellsBusy == 0 && casCellsBusy()) {
continue;
}
h = advanceProbe(h);
}
else if (cellsBusy == 0 && cells == as && casCellsBusy()) {
}
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 {
Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
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();
try {
valueOffset = unsafe.objectFieldOffset(MyAtomicInteger.class.getDeclaredField("value"));
} catch (NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
}
总结
- CAS与volatile(无锁并发)
- 重要API:原子整数、原子引用、原子数组、字段更新器、原子累加器
- unsafe 对象(底层)
- 原理:
- LongAdder源码
- 缓存行伪共享(cells累加单元数组,加入空隙,存储在不同缓存行)