深入学习掌握JUC并发编程系列(五) -- 深入浅出无锁-乐观锁

深入学习掌握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) {
            // 比如拿到了旧值 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;
            }
        }
}

深入学习掌握JUC并发编程系列(五) -- 深入浅出无锁-乐观锁_第1张图片

  • 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. 缓存行伪共享

深入学习掌握JUC并发编程系列(五) -- 深入浅出无锁-乐观锁_第2张图片
深入学习掌握JUC并发编程系列(五) -- 深入浅出无锁-乐观锁_第3张图片

  • 缓存行原理:
    • CPU 与 内存的速度差异很大,需要将数据提前读至缓存来提升效率
    • 缓存以缓存行为单位,每个缓存行对应着一块内存,64 bytes(8 个 long)
  • 缓存行弊端:
    • 产生数据副本(同一份数据会缓存在不同核心的缓存行中)
    • 保证数据的一致性:同一份数据,如果某个 CPU 核心更改了数据,其它 CPU 核心对应的整个缓存行必须失效
      深入学习掌握JUC并发编程系列(五) -- 深入浅出无锁-乐观锁_第4张图片
  • 缓存行伪共享问题:
    • 一个缓存行可以存下 2 个 Cell 对象:Cell是数组形式(在内存中连续存储),一个 Cell 24 字节(16 字节的对象头和 8 字节的 value)
    • Core-0 修改Cell[0],Core-1 修改Cell[1],无论谁修改成功,都会导致对方 Core 的缓存行失效
  • 问题解决:@sun.misc.Contended(注解)
    • 在对象或字段的前后各增加 128 字节大小的padding(空白)
    • CPU 将Cell累加单元对象预读至缓存时,占用不同的缓存行,不会造成对方缓存行的失效
      深入学习掌握JUC并发编程系列(五) -- 深入浅出无锁-乐观锁_第5张图片

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 创建的流程
    }
}

深入学习掌握JUC并发编程系列(五) -- 深入浅出无锁-乐观锁_第6张图片

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;
    }
}

深入学习掌握JUC并发编程系列(五) -- 深入浅出无锁-乐观锁_第7张图片

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累加单元数组,加入空隙,存储在不同缓存行)

你可能感兴趣的:(深入学习掌握JUC并发编程系列,学习,java)