diff --git a/week_02/65/AtomicInteger-065.md b/week_02/65/AtomicInteger-065.md new file mode 100644 index 0000000000000000000000000000000000000000..393be03a627e83680803e0c688a618b2c8801f6e --- /dev/null +++ b/week_02/65/AtomicInteger-065.md @@ -0,0 +1,58 @@ +# AtomicInteger类源码阅读笔记 + +我们在实现一个计数器的时候,很多情况下为了考虑线程安全,需要去加锁,防止计数器错乱,因为对于大多数 count++ 来说,是两步操作。两个步骤的操作,多线程必然会产生错乱的现象。而AtomicInteger 这些 concurrent 包中的计数器却不会。 + +  首先,看 AtomicInteger 类,有如下属性: + +```java +private static final Unsafe unsafe = Unsafe.getUnsafe(); +private static final long valueOffset; +private volatile int value; +``` + +  AtomicInteger的底层是通过 Unsafe 来实现的。也就是说 Unsafe 是核心。有的人会问,这里为什么可以通过 Unsafe.getUnsafe 来获取 Unsafe 的实例,因为这个类在 rt.jar 包中。系统的 classloader 加载。所以可以获取到。如果通过 ApplicationClassloader 加载,请参见下 Unsafe 源码,明显是会抛异常的。 + +  接下来看看 valueOffset 和 value 的作用: + +```java +static { + try { + valueOffset = unsafe.objectFieldOffset + (AtomicInteger.class.getDeclaredField("value")); + } catch (Exception ex) { throw new Error(ex); } +} +``` + +  初始化 valueOffset,这里的得到的是内存的偏移量。根据 offset 可以定位 jvm 中分配的内存地址。这里猜测就是定一个变量 value,然后初始化把他的内存地址放入 valueOffset 变量里。 +  这里还有一点比较重要,我们看下 value 的定义是 volatile。volatile 的意义就是 copy 出来的副本值始终和主内存中的值保持同步。即你永远读取的都是主内存中最新的值。这样子就保证了读的一致性。也就是不管多少个线程,你读取的 value 永远是一致的。 + +  通过以上解释,基本可以概括出来,通过 value,valueOffset,unsafe 这三个东西,实现了 AtomicInteger 的功能。 + +  构造函数比较简单,无参和有参。 + +```java +public AtomicInteger(int initialValue) { + value = initialValue; +} + +public AtomicInteger() { +} +``` + +getAndIncrement()方法通过usafe的CAS操作原子性地实现count++操作 + +```java +public final int getAndIncrement() { + return unsafe.getAndAddInt(this, valueOffset, 1); +} +``` + +其他方法同理 + +compareAndSet方法也是一个典型的CAS操作,用于更新value + +```java +public final boolean compareAndSet(int expect, int update) { + return unsafe.compareAndSwapInt(this, valueOffset, expect, update); +} +``` diff --git a/week_02/65/AtomicStampedReference-065.md b/week_02/65/AtomicStampedReference-065.md new file mode 100644 index 0000000000000000000000000000000000000000..a40953e6fbab718538c18e83da059c75a82be5c6 --- /dev/null +++ b/week_02/65/AtomicStampedReference-065.md @@ -0,0 +1,111 @@ +# AtomicStampedReference类源码阅读笔记 + +## ABA 问题 + +线程 1 准备用 CAS 修改变量值 A,在此之前,其它线程将变量的值由 A 替换为 B,又由 B 替换为 A,然后线程 1 执行 CAS 时发现变量的值仍然为 A,所以 CAS 成功。但实际上这时的现场已经和最初不同了。 + +![](http://upload-images.jianshu.io/upload_images/143039-6e24d206a6dfd9ce.png) + +### 例子 + +```java +public static AtomicInteger a = new AtomicInteger(1); +public static void main(String[] args){ + Thread main = new Thread(() -> { + System.out.println("操作线程" + Thread.currentThread() +",初始值 = " + a); + try { + Thread.sleep(1000); + } catch (InterruptedException e) { + e.printStackTrace(); + } + boolean isCASSuccess = a.compareAndSet(1,2); + System.out.println("操作线程" + Thread.currentThread() +",CAS操作结果: " + isCASSuccess); + },"主操作线程"); + + Thread other = new Thread(() -> { + Thread.yield(); + a.incrementAndGet(); + System.out.println("操作线程" + Thread.currentThread() +",【increment】 ,值 = "+ a); + a.decrementAndGet(); + System.out.println("操作线程" + Thread.currentThread() +",【decrement】 ,值 = "+ a); + },"干扰线程"); + + main.start(); + other.start(); +} +``` + +``` +> 操作线程Thread[主操作线程,5,main],初始值 = 1 +> 操作线程Thread[干扰线程,5,main],【increment】 ,值 = 2 +> 操作线程Thread[干扰线程,5,main],【decrement】 ,值 = 1 +> 操作线程Thread[主操作线程,5,main],CAS操作结果: true +``` + +## 解决ABA方案 + +### 思路 + +解决 ABA 最简单的方案就是给值加一个修改版本号,每次值变化,都会修改它版本号,CAS 操作时都对比此版本号。 + +![](http://upload-images.jianshu.io/upload_images/143039-ab75a4cfed405a59.png) + +### JAVA中ABA中解决方案 (AtomicStampedReference) + +AtomicStampedReference是java并发包下提供的一个原子类,它能解决其它原子类无法解决的ABA问题。 +AtomicStampedReference 主要维护包含一个对象引用以及一个可以自动更新的整数 "stamp" 的 pair 对象来解决 ABA 问题。 + +### 属性 + +内部类Pair持有一个引用以及一个stamp代表版本 + +```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); + } +} + +private volatile Pair pair; +``` + +```java +// 传入版本号数组对象 获取当前对象的实际值 无论传入的数组值为何 +//总是会返回当前对象的值并且版本号数组的第一个值为当前对象的版本号 +public V get(int[] stampHolder) { + Pair pair = this.pair; + stampHolder[0] = pair.stamp; + return pair.reference; +} +``` + +```java +// expectedReference:期望的引用 +// newReference:新的引用 +// expectedStamp:期望版本号 +// newStamp:新的版本号 +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) || // 如果新的元素和新的版本号都与旧元素相等,则不需要更新 + casPair(current, Pair.of(newReference, newStamp))); // CAS更新 +} + +private boolean casPair(Pair cmp, Pair val) { + return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val); +} +``` diff --git a/week_02/65/LongAdder-065.md b/week_02/65/LongAdder-065.md new file mode 100644 index 0000000000000000000000000000000000000000..810fed043743b6e8680c859fa8e0439afc5c329c --- /dev/null +++ b/week_02/65/LongAdder-065.md @@ -0,0 +1,213 @@ +# LongAdder源码阅读笔记 + +## 简介 + +LongAdder是jdk8新增的用于并发环境的计数器,目的是为了在高并发情况下,代替AtomicLong/AtomicInteger,成为一个用于高并发情况下的高效的通用计数器。 + +高并发下计数,一般最先想到的应该是AtomicLong/AtomicInteger,AtmoicXXX使用硬件级别的指令 CAS 来更新计数器的值,这样可以避免加锁,机器直接支持的指令,效率也很高。但是AtomicXXX中的 CAS 操作在出现线程竞争时,失败的线程会白白地循环一次,在并发很大的情况下,因为每次CAS都只有一个线程能成功,竞争失败的线程会非常多。失败次数越多,循环次数就越多,很多线程的CAS操作越来越接近 自旋锁(spin lock)。计数操作本来是一个很简单的操作,实际需要耗费的cpu时间应该是越少越好,AtomicXXX在高并发计数时,大量的cpu时间都浪费会在自旋上了,这很浪费,也降低了实际的计数效率。 + +通过阅读 LongAdder 的 Javadoc 我们了解到: + +> This class is usually preferable to {@link AtomicLong} when multiple threads update a common sum that is used for purposes such as collecting statistics, not for fine-grained synchronization control. Under low update contention, the two classes have similar characteristics. But under high contention, expected throughput of this class is significantly higher, at the expense of higher space consumption. + +大概意思就是,LongAdder 功能类似 AtomicLong ,在低并发情况下二者表现差不多,在高并发情况下 LongAdder 的表现就会好很多。 + +对于同样的一个操作,AtomicLong 只对一个 Long 值进行 CAS 操作。而 LongAdder 是针对 Cell 数组的某个 Cell 进行 CAS 操作 ,把线程的名字的 hash 值,作为 Cell 数组的下标,然后对 Cell[i] 的 long 进行 CAS 操作。简单粗暴的分散了高并发下的竞争压力。虽然原理简单粗暴,但是代码写得却相当细致和精巧。 + +## 主要方法 + +```java +public void add(long x) { + /* + 1. 如果 cells 数组不为空,对参数进行 casBase 操作,如果 casBase 操作失败。可能是竞争激烈,进入第二步。 + 2. 如果 cells 为空,直接进入 longAccumulate(); + 3. m = cells 数组长度减一,如果数组长度小于 1,则进入 longAccumulate() + 4. 如果都没有满足以上条件,则通过getProbe()对当前线程进行某种 hash 生成一个数组下标,对下标保存的值进行 cas 操作。如果操作失败,则说明竞争依然激烈,则进入 longAccumulate(). + */ + 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); + } +} +``` + +Cell 是 `java.util.concurrent.atomic` 下 `Striped64` 的一个内部类。 + +```java +@sun.misc.Contended static final class Cell { + volatile long value; + Cell(long x) { value = x; } + final boolean cas(long cmp, long val) { + return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val); + } + + private static final sun.misc.Unsafe UNSAFE; + private static final long valueOffset; + static { + try { + UNSAFE = sun.misc.Unsafe.getUnsafe(); + Class<?> ak = Cell.class; + valueOffset = UNSAFE.objectFieldOffset + (ak.getDeclaredField("value")); + } catch (Exception e) { + throw new Error(e); + } + } +} +``` + +Cell 被 @sun.misc.Contended 修饰。意思是让 Java 编译器和 JRE 运行时来决定如何填充,避免伪共享。原理是在使用此注解的对象或字段的前后各增加128字节大小的padding,使用2倍于大多数硬件缓存行的大小来避免相邻扇区预取导致的伪共享冲突。 +其实一个 Cell 的本质就是一个 volatile 修饰的 long 值,且这个值能够进行 cas 操作。 +操作的核心思想还是基于 cas。但是 cas 失败后,并不是傻乎乎的自旋,而是逐渐升级。升级的 cas 都不管用了则进入 longAccumulate() 这个方法。 + +```java +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) { + if (cellsBusy == 0) { + Cell r = new Cell(x); + if (cellsBusy == 0 && casCellsBusy()) { + boolean created = false; + try { + Cell[] rs; int m, j; + if ((rs = cells) != null && + (m = rs.length) > 0 && + rs[j = (m - 1) & h] == null) { + rs[j] = r; + created = true; + } + } finally { + cellsBusy = 0; + } + if (created) + break; + continue; + } + } + collide = false; + } + + + 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()) { + try { + if (cells == as) { + Cell[] rs = new Cell[n << 1]; + for (int i = 0; i < n; ++i) + rs[i] = as[i]; + cells = rs; + } + } finally { + cellsBusy = 0; + } + collide = false; + continue; + } + + h = advanceProbe(h); + } + + else if (cellsBusy == 0 && cells == as && casCellsBusy()) { + boolean init = false; + try { + if (cells == as) { + Cell[] rs = new Cell[2]; + rs[h & 1] = new Cell(x); + cells = rs; + init = true; + } + } finally { + cellsBusy = 0; + } + if (init) + break; + } + + else if (casBase(v = base, ((fn == null) ? v + x : + fn.applyAsLong(v, x)))) + break; + } +} +``` +longAccumulate()方法源码比较复杂。 +以下情况会进入到这个 longAccumulate 方法中, + +* cell[] 数组为空, +* cell[i] 数据的某个下标元素为空, +* casBase 失败, +* a.cas 失败, +* cell.length - 1 < 0 + +在 longAccumulate 中有几个标记位 + +* `cellsBusy` cells 的操作标记位,如果正在修改、新建、操作 cells 数组中的元素会, 会将其 cas 为 1,否则为 0。 +* `wasUncontended` 表示 cas 是否失败,如果失败则考虑操作升级。 +* `collide` 是否冲突,如果冲突,则考虑扩容 cells 的长度。 + +整个 for() 死循环,都是以 cas 操作成功而告终。否则则会修改上述描述的几个标记位,重新进入循环。 + +所以整个循环包括如下几种情况: + +1. cells 不为空 + + 1. 如果 cell[i] 某个下标为空,则 new 一个 cell,并初始化值,然后退出 + 2. 如果 cas 失败,继续循环 + 3. 如果 cell 不为空,且 cell cas 成功,退出 + 4. 如果 cell 的数量,大于等于 cpu 数量或者已经扩容了,继续重试。(扩容没意义) + 5. 设置 collide 为 true。 + 6. 获取 cellsBusy 成功就对 cell 进行扩容,获取 cellBusy 失败则重新 hash 再重试。 +2. cells 为空且获取到 cellsBusy ,init cells 数组,然后赋值退出。 + +3. cellsBusy 获取失败,则进行 baseCas ,操作成功退出,不成功则重试。 + +至此 longAccumulate 就分析完了。之所以这个方法那么复杂,我认为有两个原因 + +1. 是因为并发环境下要考虑各种操作的原子性,所以对于锁都进行了 double check。 +2. 操作都是逐步升级,以最小的代价实现功能。 + +```java +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; +} +``` + +sum()方法就很简单,就是遍历 cell 数组,累加 value 就行。 +看上去 LongAdder 性能全面超越了 AtomicLong。为什么 jdk 1.8 中还是保留了 AtomicLong 的实现呢?其实我们可以发现,LongAdder 使用了一个 cell 列表去承接并发的 cas,以提升性能,但是 LongAdder 在统计的时候如果有并发更新,可能导致统计的数据有误差。如果用于自增 id 的生成,就不适合使用 LongAdder 了。这个时候使用 AtomicLong 就是一个明智的选择。 + +LongAdder 使用了一个比较简单的原理,解决了 AtomicLong 类,在极高竞争下的性能问题。但是 LongAdder 的具体实现却非常精巧和细致,分散竞争,逐步升级竞争的解决方案,相当漂亮,值得我们细细品味。 diff --git a/week_02/65/Unsafe-065.md b/week_02/65/Unsafe-065.md new file mode 100644 index 0000000000000000000000000000000000000000..0b2c5aa521397f42d48b154372ed07c96fecc0a0 --- /dev/null +++ b/week_02/65/Unsafe-065.md @@ -0,0 +1,293 @@ + +# Unsafe类源码阅读笔记 + +Unsafe 是位于 sun.misc 包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升 Java 运行效率、增强 Java 语言底层资源操作能力方面起到了很大的作用。但由于 Unsafe 类使 Java 语言拥有了类似 C 语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用 Unsafe 类会使得程序出错的概率变大,使得 Java 这种安全的语言变得不再 “安全”,因此对 Unsafe 的使用一定要慎重。 + +# 基本介绍 + +如下 Unsafe 源码所示,Unsafe 类为一单例实现,提供静态方法 getUnsafe 获取 Unsafe 实例,当且仅当调用 getUnsafe 方法的类为引导类加载器所加载时才合法,否则抛出 SecurityException 异常。 + +```java +public final class Unsafe { + + private static final Unsafe theUnsafe; + + private Unsafe() { + } + @CallerSensitive + public static Unsafe getUnsafe() { + Class var0 = Reflection.getCallerClass(); + + if(!VM.isSystemDomainLoader(var0.getClassLoader())) { + throw new SecurityException("Unsafe"); + } else { + return theUnsafe; + } + } +} + + +``` + +那如若想使用这个类,该如何获取其实例?有如下两个可行方案。 + +其一,从`getUnsafe`方法的使用限制条件出发,通过 Java 命令行命令`-Xbootclasspath/a`把调用 Unsafe 相关方法的类 A 所在 jar 包路径追加到默认的 bootstrap 路径中,使得 A 被引导类加载器加载,从而通过`Unsafe.getUnsafe`方法安全的获取 Unsafe 实例。 + +``` +java -Xbootclasspath/a: ${path} // 其中path为调用Unsafe相关方法的类所在jar包路径 +``` + +其二,通过反射获取单例对象 theUnsafe。 + +```java +private static Unsafe reflectGetUnsafe() { + try { + Field field = Unsafe.class.getDeclaredField("theUnsafe"); + field.setAccessible(true); + return (Unsafe) field.get(null); + } catch (Exception e) { + log.error(e.getMessage(), e); + return null; + } +} + + +``` + +# 功能介绍 + +![](https://p1.meituan.net/travelcube/f182555953e29cec76497ebaec526fd1297846.png) + +如上图所示,Unsafe 提供的 API 大致可分为内存操作、CAS、Class 相关、对象操作、线程调度、系统信息获取、内存屏障、数组操作等几类,下面将对其相关方法和应用场景进行详细介绍。 + +### 内存操作 + +这部分主要包含堆外内存的分配、拷贝、释放、给定地址值操作等方法。 + +```java +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); + +public native Object getObject(Object o, long offset); + +public native void putObject(Object o, long offset, Object x); + +public native byte getByte(long address); + +public native void putByte(long address, byte x); + + +``` + +通常,我们在 Java 中创建的对象都处于堆内内存(heap)中,堆内内存是由 JVM 所管控的 Java 进程内存,并且它们遵循 JVM 的内存管理机制,JVM 会采用垃圾回收机制统一管理堆内存。与之相对的是堆外内存,存在于 JVM 管控之外的内存区域,Java 中对堆外内存的操作,依赖于 Unsafe 提供的操作堆外内存的 native 方法。 + +#### 使用堆外内存的原因 + +* 对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是 JVM,所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在 GC 时减少回收停顿对于应用的影响。 +* 提升程序 I/O 操作的性能。通常在 I/O 通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存。 + +#### 典型应用 + +DirectByteBuffer 是 Java 用于实现堆外内存的一个重要类,通常用在通信过程中做缓冲池,如在 Netty、MINA 等 NIO 框架中应用广泛。DirectByteBuffer 对于堆外内存的创建、使用、销毁等逻辑均由 Unsafe 提供的堆外内存 API 来实现。 + +下图为 DirectByteBuffer 构造函数,创建 DirectByteBuffer 的时候,通过 Unsafe.allocateMemory 分配内存、Unsafe.setMemory 进行内存初始化,而后构建 Cleaner 对象用于跟踪 DirectByteBuffer 对象的垃圾回收,以实现当 DirectByteBuffer 被垃圾回收时,分配的堆外内存一起被释放。 + +![](https://p0.meituan.net/travelcube/5eb082d2e4baf2d993ce75747fc35de6486751.png) + +那么如何通过构建垃圾回收追踪对象 Cleaner 实现堆外内存释放呢? + +Cleaner 继承自 Java 四大引用类型之一的虚引用 PhantomReference(众所周知,无法通过虚引用获取与之关联的对象实例,且当对象仅被虚引用引用时,在任何发生 GC 的时候,其均可被回收),通常 PhantomReference 与引用队列 ReferenceQueue 结合使用,可以实现虚引用关联对象被垃圾回收时能够进行系统通知、资源清理等功能。如下图所示,当某个被 Cleaner 引用的对象将被回收时,JVM 垃圾收集器会将此对象的引用放入到对象引用中的 pending 链表中,等待 Reference-Handler 进行相关处理。其中,Reference-Handler 为一个拥有最高优先级的守护线程,会循环不断的处理 pending 链表中的对象引用,执行 Cleaner 的 clean 方法进行相关清理工作。 + +![](https://p0.meituan.net/travelcube/9efac865a875c32cf570489332be5d0f131298.png) + +所以当 DirectByteBuffer 仅被 Cleaner 引用(即为虚引用)时,其可以在任意 GC 时段被回收。当 DirectByteBuffer 实例对象被回收时,在 Reference-Handler 线程操作中,会调用 Cleaner 的 clean 方法根据创建 Cleaner 时传入的 Deallocator 来进行堆外内存的释放。 + +![](https://p0.meituan.net/travelcube/66e616c6db18202578c561649facac8d387390.png) + +### CAS 相关 + +如下源代码释义所示,这部分主要为 CAS 相关操作的方法。 + +```java +public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object update); + +public final native boolean compareAndSwapInt(Object o, long offset, int expected,int update); + +public final native boolean compareAndSwapLong(Object o, long offset, long expected, long update); + + +``` + +什么是 CAS? 即比较并替换,实现并发算法时常用到的一种技术。CAS 操作包含三个操作数——内存位置、预期原值及新值。执行 CAS 操作的时候,将内存位置的值与预期原值比较,如果相匹配,那么处理器会自动将该位置值更新为新值,否则,处理器不做任何操作。我们都知道,CAS 是一条 CPU 的原子指令(cmpxchg 指令),不会造成所谓的数据不一致问题,Unsafe 提供的 CAS 方法(如 compareAndSwapXXX)底层实现即为 CPU 指令 cmpxchg。 + +#### 典型应用 + +CAS 在 java.util.concurrent.atomic 相关类、Java AQS、CurrentHashMap 等实现上有非常广泛的应用。如下图所示,AtomicInteger 的实现中,静态字段 valueOffset 即为字段 value 的内存偏移地址,valueOffset 的值在 AtomicInteger 初始化时,在静态代码块中通过 Unsafe 的 objectFieldOffset 方法获取。在 AtomicInteger 中提供的线程安全方法中,通过字段 valueOffset 的值可以定位到 AtomicInteger 对象中 value 的内存地址,从而可以根据 CAS 实现对 value 字段的原子操作。 + +![](https://p1.meituan.net/travelcube/3bacb938ca6e63d6c79c2bb48d3f608f189412.png) + +下图为某个 AtomicInteger 对象自增操作前后的内存示意图,对象的基地址 baseAddress=“0x110000”,通过 baseAddress+valueOffset 得到 value 的内存地址 valueAddress=“0x11000c”;然后通过 CAS 进行原子性的更新操作,成功则返回,否则继续重试,直到更新成功为止。 + +![](https://p0.meituan.net/travelcube/6e8b1fe5d5993d17a4c5b69bb72ac51d89826.png) + +### 线程调度 + +这部分,包括线程挂起、恢复、锁机制等方法。 + +```java +public native void unpark(Object thread); + +public native void park(boolean isAbsolute, long time); + +@Deprecated +public native void monitorEnter(Object o); + +@Deprecated +public native void monitorExit(Object o); + +@Deprecated +public native boolean tryMonitorEnter(Object o); + + +``` + +如上源码说明中,方法 park、unpark 即可实现线程的挂起与恢复,将一个线程进行挂起是通过 park 方法实现的,调用 park 方法后,线程将一直阻塞直到超时或者中断等条件出现;unpark 可以终止一个挂起的线程,使其恢复正常。 + +#### 典型应用 + +Java 锁和同步器框架的核心类 AbstractQueuedSynchronizer,就是通过调用`LockSupport.park()`和`LockSupport.unpark()`实现线程的阻塞和唤醒的,而 LockSupport 的 park、unpark 方法实际是调用 Unsafe 的 park、unpark 方式来实现。 + +### Class 相关 + +此部分主要提供 Class 和它的静态字段的操作相关方法,包含静态字段内存定位、定义类、定义匿名类、检验 & 确保初始化等。 + +```java +public native long staticFieldOffset(Field f); + +public native Object staticFieldBase(Field f); + +public native boolean shouldBeInitialized(Class c); + +public native void ensureClassInitialized(Class c); + +public native Class defineClass(String name, byte[] b, int off, int len, ClassLoader loader, ProtectionDomain protectionDomain); + +public native Class defineAnonymousClass(Class hostClass, byte[] data, Object[] cpPatches); + + +``` + +#### 典型应用 + +从 Java 8 开始,JDK 使用 invokedynamic 及 VM Anonymous Class 结合来实现 Java 语言层面上的 Lambda 表达式。 + +* **invokedynamic**: invokedynamic 是 Java 7 为了实现在 JVM 上运行动态语言而引入的一条新的虚拟机指令,它可以实现在运行期动态解析出调用点限定符所引用的方法,然后再执行该方法,invokedynamic 指令的分派逻辑是由用户设定的引导方法决定。 +* **VM Anonymous Class**:可以看做是一种模板机制,针对于程序动态生成很多结构相同、仅若干常量不同的类时,可以先创建包含常量占位符的模板类,而后通过 Unsafe.defineAnonymousClass 方法定义具体类时填充模板的占位符生成具体的匿名类。生成的匿名类不显式挂在任何 ClassLoader 下面,只要当该类没有存在的实例对象、且没有强引用来引用该类的 Class 对象时,该类就会被 GC 回收。故而 VM Anonymous Class 相比于 Java 语言层面的匿名内部类无需通过 ClassClassLoader 进行类加载且更易回收。 + +在 Lambda 表达式实现中,通过 invokedynamic 指令调用引导方法生成调用点,在此过程中,会通过 ASM 动态生成字节码,而后利用 Unsafe 的 defineAnonymousClass 方法定义实现相应的函数式接口的匿名类,然后再实例化此匿名类,并返回与此匿名类中函数式方法的方法句柄关联的调用点;而后可以通过此调用点实现调用相应 Lambda 表达式定义逻辑的功能。下面以如下图所示的 Test 类来举例说明。 + +![](https://p0.meituan.net/travelcube/7707d035eb5f04314b3684ff91dddb1663516.png) + +Test 类编译后的 class 文件反编译后的结果如下图一所示(删除了对本文说明无意义的部分),我们可以从中看到 main 方法的指令实现、invokedynamic 指令调用的引导方法 BootstrapMethods、及静态方法`lambda$main$0`(实现了 Lambda 表达式中字符串打印逻辑)等。在引导方法执行过程中,会通过 Unsafe.defineAnonymousClass 生成如下图二所示的实现 Consumer 接口的匿名类。其中,accept 方法通过调用 Test 类中的静态方法`lambda$main$0`来实现 Lambda 表达式中定义的逻辑。而后执行语句`consumer.accept("lambda")`其实就是调用下图二所示的匿名类的 accept 方法。 + +![](https://p1.meituan.net/travelcube/1038d53959701093db6c655e4a342e30456249.png) + +### 对象操作 + +此部分主要包含对象成员属性相关操作及非常规的对象实例化方式等相关方法。 + +```java +public native long objectFieldOffset(Field f); + +public native Object getObject(Object o, long offset); + +public native void putObject(Object o, long offset, Object x); + +public native Object getObjectVolatile(Object o, long offset); + +public native void putObjectVolatile(Object o, long offset, Object x); + +public native void putOrderedObject(Object o, long offset, Object x); + +public native Object allocateInstance(Class cls) throws InstantiationException; + + +``` + +#### 典型应用 + +* **常规对象实例化方式**:我们通常所用到的创建对象的方式,从本质上来讲,都是通过 new 机制来实现对象的创建。但是,new 机制有个特点就是当类只提供有参的构造函数且无显示声明无参构造函数时,则必须使用有参构造函数进行对象构造,而使用有参构造函数时,必须传递相应个数的参数才能完成对象实例化。 +* **非常规的实例化方式**:而 Unsafe 中提供 allocateInstance 方法,仅通过 Class 对象就可以创建此类的实例对象,而且不需要调用其构造函数、初始化代码、JVM 安全检查等。它抑制修饰符检测,也就是即使构造器是 private 修饰的也能通过此方法实例化,只需提类对象即可创建相应的对象。由于这种特性,allocateInstance 在 java.lang.invoke、Objenesis(提供绕过类构造器的对象生成方式)、Gson(反序列化时用到)中都有相应的应用。 + +如下图所示,在 Gson 反序列化时,如果类有默认构造函数,则通过反射调用默认构造函数创建实例,否则通过 UnsafeAllocator 来实现对象实例的构造,UnsafeAllocator 通过调用 Unsafe 的 allocateInstance 实现对象的实例化,保证在目标类无默认构造函数时,反序列化不够影响。 + +![](https://p1.meituan.net/travelcube/b9fe6ab772d03f30cd48009920d56948514676.png) + +### 数组相关 + +这部分主要介绍与数据操作相关的 arrayBaseOffset 与 arrayIndexScale 这两个方法,两者配合起来使用,即可定位数组中每个元素在内存中的位置。 + +```java +public native int arrayBaseOffset(Class arrayClass); + +public native int arrayIndexScale(Class arrayClass); + + +``` + +#### 典型应用 + +这两个与数据操作相关的方法,在 java.util.concurrent.atomic 包下的 AtomicIntegerArray(可以实现对 Integer 数组中每个元素的原子性操作)中有典型的应用,如下图 AtomicIntegerArray 源码所示,通过 Unsafe 的 arrayBaseOffset、arrayIndexScale 分别获取数组首元素的偏移地址 base 及单个元素大小因子 scale。后续相关原子性操作,均依赖于这两个值进行数组中元素的定位,如下图二所示的 getAndAdd 方法即通过 checkedByteOffset 方法获取某数组元素的偏移地址,而后通过 CAS 实现原子性操作。 + +![](https://p0.meituan.net/travelcube/160366b0fb2079ad897f6d6b1cb349cd426237.png) + +### 内存屏障 + +在 Java 8 中引入,用于定义内存屏障(也称内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是 CPU 或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作),避免代码重排序。 + +```java +public native void loadFence(); + +public native void storeFence(); + +public native void fullFence(); + + +``` + +#### 典型应用 + +在 Java 8 中引入了一种锁的新机制——StampedLock,它可以看成是读写锁的一个改进版本。StampedLock 提供了一种乐观读锁的实现,这种乐观读锁类似于无锁的操作,完全不会阻塞写线程获取写锁,从而缓解读多写少时写线程 “饥饿” 现象。由于 StampedLock 提供的乐观读锁不阻塞写线程获取读锁,当线程共享变量从主内存 load 到线程工作内存时,会存在数据不一致问题,所以当使用 StampedLock 的乐观读锁时,需要遵从如下图用例中使用的模式来确保数据的一致性。 + +![](https://p1.meituan.net/travelcube/839ad79686d06583296f3abf1bec27e3320222.png) + +如上图用例所示计算坐标点 Point 对象,包含点移动方法 move 及计算此点到原点的距离的方法 distanceFromOrigin。在方法 distanceFromOrigin 中,首先,通过 tryOptimisticRead 方法获取乐观读标记;然后从主内存中加载点的坐标值 (x,y);而后通过 StampedLock 的 validate 方法校验锁状态,判断坐标点 (x,y) 从主内存加载到线程工作内存过程中,主内存的值是否已被其他线程通过 move 方法修改,如果 validate 返回值为 true,证明 (x, y) 的值未被修改,可参与后续计算;否则,需加悲观读锁,再次从主内存加载 (x,y) 的最新值,然后再进行距离计算。其中,校验锁状态这步操作至关重要,需要判断锁状态是否发生改变,从而判断之前 copy 到线程工作内存中的值是否与主内存的值存在不一致。 + +下图为 StampedLock.validate 方法的源码实现,通过锁标记与相关常量进行位运算、比较来校验锁状态,在校验逻辑之前,会通过 Unsafe 的 loadFence 方法加入一个 load 内存屏障,目的是避免上图用例中步骤②和 StampedLock.validate 中锁状态校验运算发生重排序导致锁状态校验不准确的问题。 + +![](https://p0.meituan.net/travelcube/256f54b037d07df53408b5eea9436b34135955.png) + +### 系统相关 + +这部分包含两个获取系统相关信息的方法。 + +```java +public native int addressSize(); + +public native int pageSize(); + + +``` + +#### 典型应用 + +如下图所示的代码片段,为 java.nio 下的工具类 Bits 中计算待申请内存所需内存页数量的静态方法,其依赖于 Unsafe 中 pageSize 方法获取系统内存页大小实现后续计算逻辑。 + +![](https://p0.meituan.net/travelcube/262470b0c3e79b8f4f7b0c0280b1cc5362454.png) \ No newline at end of file