From b5a70bc1bdd68cc10abb313d0d8e7a779611cd29 Mon Sep 17 00:00:00 2001 From: shniu Date: Sun, 15 Mar 2020 18:20:36 +0800 Subject: [PATCH] 77 for week02 --- second/week_02/77/AtomicInteger.md | 133 +++++++++++++ second/week_02/77/AtomicStampedReference.md | 110 +++++++++++ second/week_02/77/LongAdder.md | 61 ++++++ second/week_02/77/Unsafe.md | 208 ++++++++++++++++++++ 4 files changed, 512 insertions(+) create mode 100644 second/week_02/77/AtomicInteger.md create mode 100644 second/week_02/77/AtomicStampedReference.md create mode 100644 second/week_02/77/LongAdder.md create mode 100644 second/week_02/77/Unsafe.md diff --git a/second/week_02/77/AtomicInteger.md b/second/week_02/77/AtomicInteger.md new file mode 100644 index 0000000..e140278 --- /dev/null +++ b/second/week_02/77/AtomicInteger.md @@ -0,0 +1,133 @@ +# AtomicInteger 源码解析 + +`AtomicInteger` 是一个可以原子更新int的类,通过使用 Unsafe 的 CAS 实现多线程安全,主要用于高并发场景。在多线程环境下,有使用 i++ 等这样的操作,但是它具有原子性问题,所以不是线程安全的,如果要达到线程安全需要使用锁的方式,如 synchronized 来保证原子性,但是 synchronized 用于此场景性能比较低,因为涉及到加锁和解锁(当然,在新的JDK版本中对 synchronized 做了锁优化,在特定场景下的性能也很高,但是对于高并发的情况下,也就是非常多的线程竞争时,synchronized 会升级为重量级锁,所以还是存在一定的性能问题,针对多线程场景下,类似 i++ 这类场景可以使用 AtomicInteger)。 + +它的核心功能有: + +```java +// Unsafe 实例,由 JDK 核心包提供,用来直接操作内存和使用CPU的原子指令 CAS +private static final Unsafe unsafe = Unsafe.getUnsafe(); +// value字段在 AtomicInteger 的偏移量,可以使用 double-register 地址模式访问对象的成员变量 +private static final long valueOffset; + +static { + try { + // 计算得到 AtomicInteger 的 value 字段的偏移量 + valueOffset = unsafe.objectFieldOffset + (AtomicInteger.class.getDeclaredField("value")); + } catch (Exception ex) { throw new Error(ex); } +} + +// value 字段,代表此类包装的值,用来做多线程并发时的计数 +// 声明为 volatile 是为了该值在多线程情况下的可见性 +private volatile int value; +// 构造函数 +public AtomicInteger(int initialValue) { + value = initialValue; +} +// 构造函数 +public AtomicInteger(int initialValue) {} + +// 原子性的更新给定的值,利用 Unsafe 的自旋 + CAS(分析unsafe.getAndSetInt可知) +public final int getAndSet(int newValue) { + // 委托给 unsafe 实例完成原子更新操作 + return unsafe.getAndSetInt(this, valueOffset, newValue); +} +// 原子性的更新给定的值,利用 Unsafe CAS(分析unsafe.compareAndSwapInt可知) +public final boolean compareAndSet(int expect, int update) { + // 委托给 unsafe 的 CAS 能力 + // this, valueOffset 加起来计算得到 value 的内存地址 + return unsafe.compareAndSwapInt(this, valueOffset, expect, update); +} + +// 原子性的自增1 +public final int getAndIncrement() { + // 委托给 unsafe + return unsafe.getAndAddInt(this, valueOffset, 1); +} +// 原子性的自减1 +public final int getAndDecrement() { + // 委托给 unsafe + return unsafe.getAndAddInt(this, valueOffset, -1); +} +// 原子性的增加 delta,返回更新前的值 +public final int getAndAdd(int delta) { + // 委托给 unsafe,自旋 + CAS + return unsafe.getAndAddInt(this, valueOffset, delta); +} +// 原子性的自增1,返回更新后的值 +public final int incrementAndGet() { + // 委托给 unsafe + return unsafe.getAndAddInt(this, valueOffset, 1) + 1; +} +// 原子性的自减1,返回更新后的值 +public final int decrementAndGet() { + // 委托给 unsafe + return unsafe.getAndAddInt(this, valueOffset, -1) - 1; +} +// 原子性的增加 delta,返回更新后的值 +public final int addAndGet(int delta) { + // 委托给 unsafe + return unsafe.getAndAddInt(this, valueOffset, delta) + delta; +} +``` + +此外,还有一些其他功能 + +```java +// IntUnaryOperator 是一个函数式接口,主要的作用是可以对更新前的值做函数变换 +// IntUnaryOperator 是一个没有副作用的函数式接口,接受一个参数返回一个值 +public final int getAndUpdate(IntUnaryOperator updateFunction) { + int prev, next; + do { + // 取内存中最新的值 + prev = get(); + // 应用函数式接口,对之前的值做变换 + next = updateFunction.applyAsInt(prev); + } while (!compareAndSet(prev, next)); // 自旋 + CAS + // 返回之前的值 + return prev; +} +public final int updateAndGet(IntUnaryOperator updateFunction) { + int prev, next; + do { + // 取内存中最新的值 + prev = get(); + // 应用函数式接口,对之前的值做变换 + next = updateFunction.applyAsInt(prev); + } while (!compareAndSet(prev, next)); // 自旋 + CAS + // 返回最新的值 + return next; +} + +// IntBinaryOperator 同样也是一个函数式接口,它是一个二元的,接收两个参数,返回一个值 +public final int getAndAccumulate(int x, IntBinaryOperator accumulatorFunction) { + int prev, next; + do { + // 取内存中最新的值 + prev = get(); + // 应用函数式接口,对之前的值做变换 + next = accumulatorFunction.applyAsInt(prev, x); + } while (!compareAndSet(prev, next)); // 自旋 + CAS + // 返回更新前的值 + return prev; +} +public final int accumulateAndGet(int x, IntBinaryOperator accumulatorFunction) { + int prev, next; + do { + // 取内存中的最新值 + prev = get(); + // 应用函数式接口,对之前的值做变换 + next = accumulatorFunction.applyAsInt(prev, x); + } while (!compareAndSet(prev, next)); // 自旋 + CAS + // 返回更新后的最新值 + return next; +} +``` + +关键点: + +1. Unsafe 实例以及 CAS 操作 +2. 自旋 + CAS 的应用,以及它在高并发情况下的优秀性能 +3. 函数式接口的应用,如 IntUnaryOperator, IntBinaryOperator 等 +4. CAS + volatile 变量,保证了变量的原子性和可见性 diff --git a/second/week_02/77/AtomicStampedReference.md b/second/week_02/77/AtomicStampedReference.md new file mode 100644 index 0000000..ad17c67 --- /dev/null +++ b/second/week_02/77/AtomicStampedReference.md @@ -0,0 +1,110 @@ +# AtomicStampedReference 源码解析 + +`AtomicStampedReference` 是一个解决 CAS 中可能出现 ABA 问题的类,通过引入版本号机制。 + +```java +// 内部类,用来存储引用对象 +private static class Pair { + // 最原始的引用对象 + final T reference; + // 为每个对象都绑定一个“邮戳”,也就是版本号 + final int stamp; + private Pair(T reference, int stamp) { + this.reference = reference; + this.stamp = stamp; + } + static Pair of(T reference, int stamp) { + return new Pair(reference, stamp); + } +} + +// 被传入的对象被包装成 Pair,自动加上版本 +private volatile Pair pair; + +// 构造函数,返回一个初始引用对象 +public AtomicStampedReference(V initialRef, int initialStamp) { + pair = Pair.of(initialRef, initialStamp); +} +``` + +基本 API + +```java +// 返回当前的引用对象 +public V getReference() { + return pair.reference; +} +// 返回当前引用对象的版本号 +public int getStamp() { + return pair.stamp; +} + +// 原子性的设置引用和版本号 +public boolean compareAndSet(V expectedReference, + V newReference, + int expectedStamp, + int newStamp) { + // 当前引用和版本号 + Pair current = pair; + return + // 比较是不是预期的引用 + expectedReference == current.reference && + // 比较是不是预期的版本号 + expectedStamp == current.stamp && + // 先看是否真的做修改了,如果没做直接返回 + ((newReference == current.reference && + newStamp == current.stamp) || + // 如果做了修改,就使用 CAS 的方式更新 + casPair(current, Pair.of(newReference, newStamp))); +} + +// 设置新的引用和新的版本号 +public void set(V newReference, int newStamp) { + // 当前的引用及其版本号 + Pair current = pair; + // 如果真的是更新了,就设置新的引用和版本号 + if (newReference != current.reference || newStamp != current.stamp) + this.pair = Pair.of(newReference, newStamp); +} + +// +public boolean attemptStamp(V expectedReference, int newStamp) { + // 当前的引用及其版本号,暂存 + Pair current = pair; + // 尝试更新版本号 + return + expectedReference == current.reference && + (newStamp == current.stamp || + casPair(current, Pair.of(expectedReference, newStamp))); +} + +// Unsafe 的实例 +private static final sun.misc.Unsafe UNSAFE = sun.misc.Unsafe.getUnsafe(); +// AtomicStampedReference 的 pair 字段的位置偏移量 +private static final long pairOffset = + objectFieldOffset(UNSAFE, "pair", AtomicStampedReference.class); + +// 使用 CAS 指令更新对象(Pair是对象) +private boolean casPair(Pair cmp, Pair val) { + // 委托给 Unsafe 的 CAS Object 操作,保证了原子性 + return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val); +} + +static long objectFieldOffset(sun.misc.Unsafe UNSAFE, + String field, Class klazz) { + try { + return UNSAFE.objectFieldOffset(klazz.getDeclaredField(field)); + } catch (NoSuchFieldException e) { + // Convert Exception to corresponding Error + NoSuchFieldError error = new NoSuchFieldError(field); + error.initCause(e); + throw error; + } +} +``` + +可见,AtomicStampedReference 是通过维护对象及其版本号来解决ABA问题的,如果在不存在ABA的场景,可以不实用此类。 + +参考 + +[死磕 java并发包之AtomicStampedReference源码分析](https://zhuanlan.zhihu.com/p/65240318) diff --git a/second/week_02/77/LongAdder.md b/second/week_02/77/LongAdder.md new file mode 100644 index 0000000..6352bf3 --- /dev/null +++ b/second/week_02/77/LongAdder.md @@ -0,0 +1,61 @@ +# LongAdder 源码解析 + +LongAdder 是一个可以应对高并发的“计数器”,而 AtomicLong 同样也可以,但是制约 AtomicLong 高效的原因是高并发,高并发意味着 CAS 的失败几率更高, 重试次数更多,越多线程重试,CAS 失败几率又越高,变成恶性循环,AtomicLong 效率降低。 那怎么解决? LongAdder 给了我们一个非常容易想到的解决方案:减少并发,将单一 value 的更新压力分担到多个 value 中去,降低单个value的 “热度”,分段更新! + +从源码层面来分析一下 + +```java +// 继承自 Striped64,并且大部分的能力是从 Striped64 继承过来的,Striped64 很重要 +public class LongAdder extends Striped64 implements Serializable { + // ... + // add 是 LongAdder 的核心方法,用于累加给定值 + public void add(long x) { + // 声明一些局部变量 + Cell[] as; long b, v; int m; Cell a; + // cells 是 Striped64 的成员变量,casBase(...) 在这里是关键 + 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); + } + } + + // ... + // sum 的值包含两部分 + // 1.来自 base + // 2.来自 cells,cells 主要用来降低高并发情况下的更新冲突,使用多个 cell 来降低冲突 + public long sum() { + Cell[] as = cells; Cell a; + // base 是在不存在并发下更新的值 + long sum = base; + if (as != null) { + // 如果cells不为空,表示有并发更新 + for (int i = 0; i < as.length; ++i) { + if ((a = as[i]) != null) + // 累加 + sum += a.value; + } + } + // 可见,当存在并发时,调用 sum 函数得到的不是精确值,而是在某一个时刻的快照 + return sum; + } +} +``` + +LongAdder 的设计思想值得我们细细品味,分场景优化,通过分段来降低冲突的概率。 + +接下来就是对 Striped64 类的分析 + +## LongAdder VS AtomicLong + +看上去 LongAdder 性能全面超越了 AtomicLong。为什么 jdk 1.8 中还是保留了 AtomicLong 的实现呢? +其实我们可以发现,LongAdder 使用了一个 cell 列表去承接并发的 cas,以提升性能,但是 LongAdder 在统计的时候如果有并发更新,可能导致统计的数据有误差。 +如果用于自增 id 的生成,就不适合使用 LongAdder 了。这个时候使用 AtomicLong 就是一个明智的选择。 +而在 Sentinel 中 LongAdder 承担的只是统计任务,且允许误差。 + +参考 + +1. [深入剖析LongAdder是咋干活的](https://juejin.im/post/5d2eb113518825305f248079) diff --git a/second/week_02/77/Unsafe.md b/second/week_02/77/Unsafe.md new file mode 100644 index 0000000..e19715b --- /dev/null +++ b/second/week_02/77/Unsafe.md @@ -0,0 +1,208 @@ +# Unsafe 源码解析 + +> A collection of methods for performing low-level, unsafe operations. Although the class and all methods are public, use of this class is limited because only trusted code can obtain instances of it. + +这是 Unsafe 类的注释,可见 JDK 是不推荐我们直接使用 Unsafe 的实例的,它是一系列低层次(比如针对内存的直接操作)操作的方法集合;所谓的 Unsafe 的含义是该类的实例可以绕开 JVM 的束缚直接操作内存,并且提供 CPU 指令 CAS 原子操作级别的支持,如果被一些不了解情况的人使用,会出现意想不到的异常情况,往往是致命的。 + +注:想想为何需要 CAS 原子级别操作?为何要直接操作内存呢,交给 JVM 去操作不行吗? + +Java 的设计是不能直接操作底层操作系统,而是通过 native 方法去操作,这也是 JVM 的核心能力;但是 Unsafe 提供了一些更加高效的方式来使用 CPU 指令和操作内存空间。 + +[Unsafe 的线上源码地址](http://hg.openjdk.java.net/jdk/jdk/file/a1ee9743f4ee/jdk/src/share/classes/sun/misc/Unsafe.java) + +## Unsafe 实例 + +```java +// 私有化构造函数,禁止了直接 new +private Unsafe() {} +// 静态的不可变对象 +private static final Unsafe theUnsafe = new Unsafe(); +@CallerSensitive +public static Unsafe getUnsafe() { // 获取Unsafe实例 + // 判断类加载器是不是 Bootstrap ClassLoader,不是的话直接抛出不安全的异常 + Class caller = Reflection.getCallerClass(); + if (!VM.isSystemDomainLoader(caller.getClassLoader())) + throw new SecurityException("Unsafe"); + return theUnsafe; +} + +// 使用 +Unsafe U = Unsafe.getUnsafe(); +``` + +JDK 提供了一个 Unsafe 的单例对象,供JDK的其他类去使用,比如在 JUC 包中大量使用;从源码中可以看出使用单例模式来保证只有一个Unsafe的实例,自己最好不要使用反射机制去获取Unsafe实例或者创建新的Unsafe实例。 + +## Unsafe 的功能分类 + +![Unsafe功能分类](https://p1.meituan.net/travelcube/f182555953e29cec76497ebaec526fd1297846.png) + +### 对象操作 + +```java +//返回对象成员属性在内存地址相对于此对象的内存地址的偏移量 +public native long objectFieldOffset(Field f); +//获得给定对象的指定地址偏移量的值,与此类似操作还有:getInt,getDouble,getLong,getChar等 +public native Object getObject(Object o, long offset); +//给定对象的指定地址偏移量设值,与此类似操作还有:putInt,putDouble,putLong,putChar等 +public native void putObject(Object o, long offset, Object x); +//有序、延迟版本的putObjectVolatile方法,不保证值的改变被其他线程立即看到。只有在field被volatile修饰符修饰时有效 +public native void putOrderedObject(Object o, long offset, Object x); +//绕过构造方法、初始化代码来创建对象 +public native Object allocateInstance(Class cls) throws InstantiationException; +// 本地方法,得搞明白 o 是什么,offset 又是什么 +public native int getInt(Object o, long offset); +``` + +这个方法的是在给定的对象中,通过offset来获取对应位置成员变量的值。可以分为三种场景: + +1. 获取实例对象的成员 +2. 获取类的静态成员 +3. 获取数组类型的成员 + +### CAS 操作 + +`double-regiseter` 模式是一种双地址模式,需要 o 和 offset 一起配合才能找到要操作的对象(或者内存地址),JVM 堆中的对象一般需要这种方式来操作 + +```java +// 对象的 CAS 操作,o 的 offset 位置是一个对象成员,如果是excepted就更新为x +public final native boolean compareAndSwapObject(Object o, long offset, + Object expected, + Object x); +// 对象的 CAS 操作,o 的 offset 位置是一个int类型的成员 +public final native boolean compareAndSwapInt(Object o, long offset, + int expected, + int x); +// 对象的 CAS 操作,o 的 offset 位置是一个long类型的成员 +public final native boolean compareAndSwapLong(Object o, long offset, + long expected, + long x); + +// 自旋+CAS的典型用法,lock-free算法的精髓所在 +public final int getAndAddInt(Object o, long offset, int delta) { + int v; + // 自旋判断是期望的最新值就更新,如果不是就自旋,直到成功 + do { + // 取到最新的值 + v = getIntVolatile(o, offset); + // 尝试增加 delta,因为在多线程情况下,有可能存在竞态条件,使用自旋不断尝试 + } while (!compareAndSwapInt(o, offset, v, v + delta)); + return v; +} +// 和 getAndAddInt 类似 +public final long getAndAddLong(Object o, long offset, long delta) { + long v; + do { + v = getLongVolatile(o, offset); + } while (!compareAndSwapLong(o, offset, v, v + delta)); + return v; +} +// 原子性的设置指定位置的值为新的值,同样是自旋 + CAS +public final int getAndSetInt(Object o, long offset, int newValue) { + int v; + do { + v = getIntVolatile(o, offset); + } while (!compareAndSwapInt(o, offset, v, newValue)); + return v; +} +public final long getAndSetLong(Object o, long offset, long newValue) { + long v; + do { + v = getLongVolatile(o, offset); + } while (!compareAndSwapLong(o, offset, v, newValue)); + return v; +} + public final Object getAndSetObject(Object o, long offset, Object newValue) { + Object v; + do { + v = getObjectVolatile(o, offset); + } while (!compareAndSwapObject(o, offset, v, newValue)); + return v; +} +``` + +`CAS` 是由 CPU 提供的原子指令级别的操作(cpu指令 cmpxchg),所谓原子指令是 CPU 硬件保证了该操作的原子性,所以不必担心原子性被破坏的问题,不会引发数据不一致问题;`CAS` 的意思是先比较再替换,包含三个操作数——内存位置、预期原值及新值。 + +### 线程调度操作 + +```java +// 这是和线程调度相关的本地方法,和线程的状态切换有关系 +// unpark是将某个线程从 WAITING or TIMED_WAITING 状态激活,线程重新可以被调度,在就绪队列排队 +// 竞争CPU +public native void unpark(Object thread); +// park将某个线程从Runnable状态转成 WAITING or TIMED_WAITING,线程等待被激活或者超时时间到后进入就绪队列 +public native void park(boolean isAbsolute, long time); +``` + +关于 `park` 和 `unpark` 的基本能力是线程的挂起和恢复,其中在 AQS 中得到充分的应用,通过 `LockSupport.park` 和 `LockSupport.unpark` 实现,而 `LockSupport` 又基于 `Unsafe` 实现。 + +看到这里不妨去翻一下 `LockSupport` 的代码 + +### volatile 操作 + +```java +// 从对象的指定偏移量处获取变量的引用,使用volatile的加载语义 +public native Object getObjectVolatile(Object o, long offset); +// 存储变量的引用到对象的指定的偏移量处,使用volatile的存储语义 +public native void putObjectVolatile(Object o, long offset, Object x); +public native int getIntVolatile(Object o, long offset); +public native void putIntVolatile(Object o, long offset, int x); +// ... +``` + +`volatile` 的语义是禁用缓存,禁用指令重排序,底层是使用内存屏障的方式实现的;CPU 和内存之间还有一层高速缓存,CPU直接操作的数据是在缓存中的,有时候未必会及时刷新到内存中,而 `volatile` 类型的变量高速CPU,改了这个值要强制刷新到内存里,取这个值的时候也去内存再取最新的出来。这样在多线程并发的场景中,对于 `voliatile` 变量的写 happens-before 对这个变量的读,就解决了可见性问题。 + +这些方法让不是 `volatile` 的变量也有 `volatile` 的能力。 + +### 内存屏障 + +内存屏障是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。 + +```java +// 内存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前 +// 这就保证了对某个变量的读,然后才能写 +public native void loadFence(); +// 内存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前 +// 这就保证了对某个变量的写,然后才能读 +public native void storeFence(); +//内存屏障,禁止load、store操作重排序 +public native void fullFence(); +``` + +`StampedLock` 是一个改进版的读写锁,内部使用就使用了内存屏障,可以去看看。 + +### 内存管理(堆外内存) + +```java +//分配内存, 相当于C++的malloc函数 +public native long allocateMemory(long bytes); +//扩充内存 +public native long reallocateMemory(long address, long bytes); +//释放内存 +public native void freeMemory(long address); +//在给定的内存块中设置值 +public native void setMemory(Object o, long offset, long bytes, byte value); +//内存拷贝 +public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes); +//获取给定地址值,忽略修饰限定符的访问限制。与此类似操作还有: getInt,getDouble,getLong,getChar等 +public native Object getObject(Object o, long offset); +//为给定地址设置值,忽略修饰限定符的访问限制,与此类似操作还有: putInt,putDouble,putLong,putChar等 +public native void putObject(Object o, long offset, Object x); +//获取给定地址的byte类型的值(当且仅当该内存地址为allocateMemory分配时,此方法结果为确定的) +public native byte getByte(long address); +//为给定地址设置byte类型的值(当且仅当该内存地址为allocateMemory分配时,此方法结果才是确定的) +public native void putByte(long address, byte x); +``` + +主要针对堆外内存的分配、拷贝、释放、操作内存等;通常,我们在Java中创建的对象都处于堆内内存(heap)中,堆内内存是由JVM所管控的Java进程内存,并且它们遵循JVM的内存管理机制,JVM会采用垃圾回收机制统一管理堆内存。与之相对的是堆外内存,存在于JVM管控之外的内存区域,Java中对堆外内存的操作,依赖于Unsafe提供的操作堆外内存的native方法。 + +为什么要使用堆外内存? + +1. 对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是JVM,所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在GC时减少回收停顿对于应用的影响。 +2. 提升程序I/O操作的性能。通常在I/O通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存。 + +### 参考资料 + +- [Java魔法类:Unsafe应用解析](https://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html) +- [JDK Unsafe 源码完全注释](https://my.oschina.net/editorial-story/blog/3019773) +- [Unsafe source code](http://hg.openjdk.java.net/jdk/jdk/file/a1ee9743f4ee/jdk/src/share/classes/sun/misc/Unsafe.java) +- [Java Magic. Part 4: sun.misc.Unsafe](http://ifeve.com/sun-misc-unsafe/) -- Gitee