From 5a036083cae12aff7c34b09b991f8b15ba9afce1 Mon Sep 17 00:00:00 2001 From: RDCynthia Date: Fri, 13 Mar 2020 20:06:45 +0800 Subject: [PATCH] week 02 homework commit --- second/week_02/81/AtomicInteger.md | 478 ++++++++ second/week_02/81/AtomicStampedReference.md | 408 +++++++ second/week_02/81/Unsafe.md | 1140 +++++++++++++++++++ 3 files changed, 2026 insertions(+) create mode 100644 second/week_02/81/AtomicInteger.md create mode 100644 second/week_02/81/AtomicStampedReference.md create mode 100644 second/week_02/81/Unsafe.md diff --git a/second/week_02/81/AtomicInteger.md b/second/week_02/81/AtomicInteger.md new file mode 100644 index 0000000..9520f67 --- /dev/null +++ b/second/week_02/81/AtomicInteger.md @@ -0,0 +1,478 @@ +#### 简述 + +```java +public class AtomicInteger extends Number implements java.io.Serializable +``` + +一个可以自动更新的 int 值。有关原子变量的属性的描述,请参见 [java.util.concurrent.atomic](https://docs.oracle.com/javase/8/docs/api/) 软件包规范。AtomicInteger 用于原子递增计数器之类的应用程序,并且不能用作 Integer 的替代品。但是,此类确实扩展了 Number,以允许处理基于数字的类的工具和实用程序进行统一访问。 + +**java.util.concurrent.atomic 描述** + +一个小的类工具包,支持对单个变量进行无锁线程安全编程。该程序包中的类将 volatile 的值,字段和数组元素的概念扩展到那些还提供了以下形式的原子条件的更新操作: + +```java +boolean compareAndSet(expectedValue, updateValue); +``` + +定义新的实用函数很简单,就像 getAndIncrement 一样,将一个函数自动应用于一个值。 例如,进行一些转换 + +```java +long transform(long input) +``` + +如下编写您的实用程序方法: + +```java +long getAndTransform(AtomicLong var) { + long prev, next; + do { + prev = var.get(); + next = transform(prev); + } while (!var.compareAndSet(prev, next)); + return prev; // return next; for transformAndGet + } +``` + +原子性访问和更新内存效果,与volatiles遵循同样的规则,如 The [Java Language Specification (17.4 Memory Model)](https://docs.oracle.com/javase/8/docs/api/) 所述: + ++ get 与 volatile 读效果一样 ++ set 与 volatile 写效果一样 ++ lazySet 具有写入(分配) volatile 变量的内存效果,除了它允许对后续(但不是先前)的内存操作进行重排序,而这些内存操作本身不会对普通的 非volatile 写入施加强加约束。 ++ compareAndSet 和所有其他读取和更新操作(如getAndIncrement)一样,与 volatile 变量读取和写入具有相同的内存效果。 + +> lazySet是使用Unsafe.putOrderedObject方法,这个方法在对低延迟代码是很有用的,它能够实现非阻塞的写入,这些写入不会被Java的JIT重新排序指令(instruction reordering),这样它使用快速的存储-存储(store-store) barrier, 而不是较慢的存储-加载(store-load) barrier, 后者总是用在volatile的写操作上,这种性能提升是有代价的,虽然便宜,也就是写后结果并不会被其他线程看到,甚至是自己的线程,通常是几纳秒后被其他线程看到,这个时间比较短,所以代价可以忍受。 + 设想如下场景: 设置一个 volatile 变量为 null,让这个对象被 GC 掉,volatile write 是消耗比较大(store-load 屏障)的,但是 putOrderedInt 只会加 store-store 屏障,损耗会小一些。 + +除了表示单个值的类之外,package中还包含Updater类,可用于在类的volatile字段上执行compareAndSet操作。 AtomicReferenceFieldUpdater、AtomicIntegerFieldUpdater和AtomicLongFieldUpdater是基于反射的,可提供对相关字段类型的访问。这些主要用于原子数据结构,同一节点的几个volatile字段(例如,树节点的链接)独立地原子更新。这些类在如何以及何时使用原子更新方面提供了更大的灵活性,但代价是更加笨拙的基于反射的设置、更不方便的使用和更弱的保证。 + +AtomicIntegerArray、AtomicLongArray和AtomicReferenceArray类进一步将原子操作支持扩展到这些类型的数组。这些类还提供了数组元素的volatile访问语义,这是普通数组不支持的。 + +AtomicMarkableReference 类将单个布尔值与引用相关联。例如,该位可能在数据结构中使用,表示被引用的对象在逻辑上已被删除。AtomicStampedReference类将整数值与引用相关联。例如,这可以用于表示与一系列更新相对应的版本号。 + +原子类主要设计为用于实现非阻塞数据结构和相关基础结构类的构建。 compareAndSet方法不是锁定的一般替代方法。 仅当对象的关键更新仅限于单个变量时,它才适用。 + +原子类不是 java.lang.Integer 和相关类的通用替换。它们没有定义 equals,hashCode 和 compareTo 等方法(由于原子变量预期会发生变化,所以它们不适合作为哈希表键)。 + +#### 源码分析 + +J.U.C 并发包中,大量使用了 sun.misc.Unsafe 类. + +```java +package java.util.concurrent.atomic; +import java.util.function.IntUnaryOperator; +import java.util.function.IntBinaryOperator; +import sun.misc.Unsafe; + +public class AtomicInteger extends Number implements java.io.Serializable { + private static final long serialVersionUID = 6214790243416807050L; + + // setup to use Unsafe.compareAndSwapInt for updates + //使用 Unsafe 类中的 compareAndSwapInt 方法来执行更新操作 + private static final Unsafe unsafe = Unsafe.getUnsafe(); + //变量 value 在 AtomicInteger 类中的偏移量(关于偏移量,需要先明白 Unsafe 类的操作原理) + private static final long valueOffset; + + static { + try { + //通过 Unsafe 类的 objectFieldOffset 获取 value 成员变量的偏移量 + valueOffset = unsafe.objectFieldOffset + (AtomicInteger.class.getDeclaredField("value")); + } catch (Exception ex) { throw new Error(ex); } + } + + //要操作的值,被 volatile 修饰,具有可见性与有序性 + private volatile int value; + + /** + * Creates a new AtomicInteger with the given initial value. + * + * 使用给定值创建一个新的 AtomicInteger + * + * @param initialValue the initial value + */ + public AtomicInteger(int initialValue) { + value = initialValue; + } + + /** + * Creates a new AtomicInteger with initial value {@code 0}. + * + * 创建一个新的 AtomicInteger ,其 value 初始值为 0 + */ + public AtomicInteger() { + } + + /** + * Gets the current value. + * + * 获取当前的值 + * + * @return the current value + */ + public final int get() { + return value; + } + + /** + * Sets to the given value. + * + * 设置当前的值 + * + * @param newValue the new value + */ + public final void set(int newValue) { + value = newValue; + } + + /** + * Eventually sets to the given value. + * + * 最终设置为给定值。 + * + * @param newValue the new value + * @since 1.6 + */ + public final void lazySet(int newValue) { + // putOrderedInt 方法,参考 Unsafe 源码 + unsafe.putOrderedInt(this, valueOffset, newValue); + } + + /** + * Atomically sets to the given value and returns the old value. + * + * 以原子方式设置为给定值并返回旧值。返回旧值 + * + * @param newValue the new value + * @return the previous value + */ + public final int getAndSet(int newValue) { + return unsafe.getAndSetInt(this, valueOffset, newValue); + } + + /** + * Atomically sets the value to the given updated value + * if the current value {@code ==} the expected value. + * + * 如果 当前值(value) == 期望值(expect),则以原子方式将该值设置为给定的更新值(update)。 + * + * @param expect the expected value + * @param update the new value + * @return {@code true} if successful. False return indicates that + * the actual value was not equal to the expected value. + * + * 如果成功,则为true。错误返回表示实际值不等于期望值。 + */ + public final boolean compareAndSet(int expect, int update) { + // unsafe 的 CAS 操作 + return unsafe.compareAndSwapInt(this, valueOffset, expect, update); + } + + /** + * Atomically sets the value to the given updated value + * if the current value {@code ==} the expected value. + * + *

May fail + * spuriously and does not provide ordering guarantees, so is + * only rarely an appropriate alternative to {@code compareAndSet}. + * + * @param expect the expected value + * @param update the new value + * @return {@code true} if successful + */ + public final boolean weakCompareAndSet(int expect, int update) { + return unsafe.compareAndSwapInt(this, valueOffset, expect, update); + } + + /** + * Atomically increments by one the current value. + * + * 以原子方式将当前值增加一。返回旧值 + * + * @return the previous value + */ + public final int getAndIncrement() { + return unsafe.getAndAddInt(this, valueOffset, 1); + } + + /** + * Atomically decrements by one the current value. + * + * 以原子方式将当前值减一。返回旧值 + * + * @return the previous value + */ + public final int getAndDecrement() { + return unsafe.getAndAddInt(this, valueOffset, -1); + } + + /** + * Atomically adds the given value to the current value. + * + * 以原子方式将给定值添加到当前值。返回旧值 + * + * @param delta the value to add + * @return the previous value + */ + public final int getAndAdd(int delta) { + return unsafe.getAndAddInt(this, valueOffset, delta); + } + + /** + * Atomically increments by one the current value. + * + * 以原子方式将当前值增加一。返回新值 + * + * @return the updated value + */ + public final int incrementAndGet() { + return unsafe.getAndAddInt(this, valueOffset, 1) + 1; + } + + /** + * Atomically decrements by one the current value. + * + * 以原子方式将当前值减一。返回新值 + * + * @return the updated value + */ + public final int decrementAndGet() { + return unsafe.getAndAddInt(this, valueOffset, -1) - 1; + } + + /** + * Atomically adds the given value to the current value. + * + * 以原子方式将给定值添加到当前值。返回新值 + * + * @param delta the value to add + * @return the updated value + */ + public final int addAndGet(int delta) { + return unsafe.getAndAddInt(this, valueOffset, delta) + delta; + } + + /** + * Atomically updates the current value with the results of + * applying the given function, returning the previous value. The + * function should be side-effect-free, since it may be re-applied + * when attempted updates fail due to contention among threads. + * + * 使用给定函数的结果以原子方式更新当前值,并返回先前的值。 + * 该函数应无副作用,因为当尝试更新由于线程间争用而失败时,可能会重新应用该函数。 + * 返回旧值 + * + * @param updateFunction a side-effect-free function + * @return the previous value + * @since 1.8 + */ + public final int getAndUpdate(IntUnaryOperator updateFunction) { + int prev, next; + //将函数返回的新值,替换为旧值。如果 CAS 失败,则会一直进行尝试,所以这个函数应该没有副作用 + do { + //获取旧值 + prev = get(); + //调用无副作用函数,生成新值 + next = updateFunction.applyAsInt(prev); + } while (!compareAndSet(prev, next)); //使用 CAS,将旧值替换为新值 + return prev; + } + + /** + * Atomically updates the current value with the results of + * applying the given function, returning the updated value. The + * function should be side-effect-free, since it may be re-applied + * when attempted updates fail due to contention among threads. + * + * 使用给定函数的结果以原子方式更新当前值,并返回更新后的值。 + * 该函数应无副作用,因为当尝试更新由于线程间争用而失败时,可能会重新应用该函数。 + * 返回新值 + * + * @param updateFunction a side-effect-free function + * @return the updated value + * @since 1.8 + */ + //源码分析参考 getAndUpdate 方法 + public final int updateAndGet(IntUnaryOperator updateFunction) { + int prev, next; + do { + prev = get(); + next = updateFunction.applyAsInt(prev); + } while (!compareAndSet(prev, next)); + return next; + } + + /** + * Atomically updates the current value with the results of + * applying the given function to the current and given values, + * returning the previous value. The function should be + * side-effect-free, since it may be re-applied when attempted + * updates fail due to contention among threads. The function + * is applied with the current value as its first argument, + * and the given update as the second argument. + * + * 将给定函数应用于当前值(value)和给定值(x),以原子方式更新当前值,并返回先前的值。 + * 该函数应无副作用,因为当尝试更新由于线程间争用而失败时,可能会重新应用该函数。 + * 应用该函数时,将当前值作为其第一个参数,并将给定的 x 作为第二个参数。 + * 返回旧值 + * + * @param x the update value + * @param accumulatorFunction a side-effect-free function of two arguments + * @return the previous value + * @since 1.8 + */ + public final int getAndAccumulate(int x, + IntBinaryOperator accumulatorFunction) { + int prev, next; + do { + prev = get(); + next = accumulatorFunction.applyAsInt(prev, x); + } while (!compareAndSet(prev, next)); + return prev; + } + + /** + * Atomically updates the current value with the results of + * applying the given function to the current and given values, + * returning the updated value. The function should be + * side-effect-free, since it may be re-applied when attempted + * updates fail due to contention among threads. The function + * is applied with the current value as its first argument, + * and the given update as the second argument. + * + * 将给定函数应用于当前值和给定值,以原子方式更新当前值,并返回更新后的值。 + * 该函数应无副作用,因为当尝试更新由于线程间争用而失败时,可能会重新应用该函数。 + * 应用该函数时,将当前值作为其第一个参数,并将给定的 x 作为第二个参数。 + * 返回新值 + * + * @param x the update value + * @param accumulatorFunction a side-effect-free function of two arguments + * @return the updated value + * @since 1.8 + */ + public final int accumulateAndGet(int x, + IntBinaryOperator accumulatorFunction) { + int prev, next; + do { + prev = get(); + next = accumulatorFunction.applyAsInt(prev, x); + } while (!compareAndSet(prev, next)); + return next; + } + + //以下是各种 int 类型转换方法 + + /** + * Returns the String representation of the current value. + * @return the String representation of the current value + */ + public String toString() { + return Integer.toString(get()); + } + + /** + * Returns the value of this {@code AtomicInteger} as an {@code int}. + */ + public int intValue() { + return get(); + } + + /** + * Returns the value of this {@code AtomicInteger} as a {@code long} + * after a widening primitive conversion. + * @jls 5.1.2 Widening Primitive Conversions + */ + public long longValue() { + return (long)get(); + } + + /** + * Returns the value of this {@code AtomicInteger} as a {@code float} + * after a widening primitive conversion. + * @jls 5.1.2 Widening Primitive Conversions + */ + public float floatValue() { + return (float)get(); + } + + /** + * Returns the value of this {@code AtomicInteger} as a {@code double} + * after a widening primitive conversion. + * @jls 5.1.2 Widening Primitive Conversions + */ + public double doubleValue() { + return (double)get(); + } + +} +``` + +#### 测试用例 + +运行 100 个并发线程,每个线程计数自增 100 次,查看计数结果是否等于 100 * 100,也就是 10000. + +**普通 int 类型** + +```java +public class OtherTest { + + private static int count = 0; //原始基本类型 + + private static void incr() { + count++; //计数自增 + } + + public static void main(String[] args) { + //运行 100 个并发线程,每个线程计数自增 100 次 + IntStream.range(0, 100).forEach(i -> new Thread(() -> IntStream.range(0, 100).forEach(j -> incr())).start()); + //debug 下 > 1 , run 下 > 2 + //当活动线程只剩下主线程后,退出循环 + while (Thread.activeCount() > 1) { + //让出 CPU 切片 + Thread.yield(); + } + System.out.println(count); + } +} +``` + +多次运行后,发现每次结果都不一样。相同的是,每次都达不到 10000。如果将 count 使用 volatile 关键词修饰呢? + +```java + private static volatile int count = 0; + + private static void incr() { + //这里,如果你用 intellij IDE 的话,会有黄色警告 : Non-atomic operation on volatile field 'count' + //就是说,count 操作不是原子性的 + count++; + } +``` + +同样多次运行后,结果依然是不同的,同时也达不到 10000。 +虽然 volatile 保证了可见性,即一个线程对共享变量的修改对其他线程可见,但是由于 count++ 这个操作不是原子性的,所以无法保证其获取到的值与修改的值是相同的。 + +**AtomicInteger 原子类型** + +```java +public class Test { + + private static AtomicInteger count = new AtomicInteger(0); + + private static void incr() { + count.incrementAndGet(); + } + + public static void main(String[] args) { + IntStream.range(0, 100).forEach(i -> new Thread(() -> IntStream.range(0, 100).forEach(j -> incr())).start()); + while (Thread.activeCount() > 1) { + Thread.yield(); + } + System.out.println(count); + } +} +``` + +多次运行后,结果都为 10000。 + +#### 引申问题 + +AtomicInteger 使用 CAS 无锁的方式解决并发修改效率很高,其结果也让人满意,但是在某些场景下,却有一个很大的问题,那就是 ABA 问题。 \ No newline at end of file diff --git a/second/week_02/81/AtomicStampedReference.md b/second/week_02/81/AtomicStampedReference.md new file mode 100644 index 0000000..4ee439b --- /dev/null +++ b/second/week_02/81/AtomicStampedReference.md @@ -0,0 +1,408 @@ +#### 简述 + +```java +public class AtomicStampedReference extends Object {} +``` + +AtomicStampedReference 维护对象引用以及可以自动更新的整数 "stamp". + +实现说明:此实现通过创建内部对象(表示为 "boxed" \[reference, integer\]对 )来维护标记的引用。 + +#### 什么是 ABA 问题 + +在CAS算法中,需要取出内存中某时刻的数据(由用户完成),在下一时刻比较并替换(由CPU完成,该操作是原子的)。在并发环境下的这个时间差中,会导致数据的变化。 + +例如: + +1. 线程 1 从内存位置 V 中取出 A。 +2. 线程 2 从位置 V 中取出 A。 +3. 线程 2 进行了一些操作,将 B 写入位置 V。 +4. 线程 2 将 A 再次写入位置 V。 +5. 线程 1 进行CAS操作,发现位置 V 中仍然是 A,操作成功。 + +尽管线程 1 的 CAS 操作成功,但不代表这个过程没有问题——对于线程 1 ,线程 2 的修改已经丢失。 + +**代码示例** + +比如现在有一个无锁的栈结构: + +```java +import javax.annotation.Nullable; +import java.util.concurrent.atomic.AtomicReference; + +public class OtherTest { + + // 原子操作 + private AtomicReference stack = new AtomicReference<>(); + + //入栈 + private void push(@Nullable Node newNode) { + if (newNode != null) { + Node node; + do { + node = stack.get(); + newNode.next = node; + } while (!stack.compareAndSet(node, newNode)); + } + } + + //出栈 + private Node pop(boolean isSleep) { + Node node, next; + do { + node = stack.get(); + if (node == null) return null; + next = node.next; + + //模拟线程阻塞 + try { + if (isSleep) Thread.sleep(1000); + } catch (Exception e) { + // nothing to do + } + + } while (!stack.compareAndSet(node, next)); + node.next = null; + return node; + } + + //打印栈 + private void output() { + Node node = stack.get(); + while (node != null) { + System.out.println(node.value); + node = node.next; + } + } + + private static class Node { + int value; + Node next; + + Node(int value) { + this.value = value; + } + + } + + public static void main(String[] args) { + OtherTest ot = new OtherTest(); + + //先初始化三个值 + ot.push(new Node(3)); + ot.push(new Node(2)); + ot.push(new Node(1)); + + //线程 1 操作 + new Thread(() -> ot.pop(true)).start(); + + //线程 2 操作 + new Thread(() -> { + Node node = ot.pop(false); + ot.pop(false); + ot.push(node); + }).start(); + + while (Thread.activeCount() > 1) { + Thread.yield(); + } + + ot.output(); + } +} +``` + +1. 初始化栈数据为 1 -> 2 -> 3 +2. 线程 1 执行 pop 操作,并在 `compareAndSet` 处阻塞 +3. 线程 2 执行 pop 操作,此时数据为 2 -> 3 +4. 线程 2 执行 pop 操作,此时数据为 3 +5. 线程 2 执行 push 操作,将第 3 步出栈的 1 入栈,此时数据为 1 -> 3 +6. 线程 1 恢复执行,比较 1 数据结点,发现没有变化(由于线程 1 持有 Node 引用,所以线程 2 的修改对 Node 引用可见) +7. 执行 CAS 成功,此时数据为 2 + +为什么是 2 呢? +1. 线程 1 保留了本地变量 next ,而 next 一开始指向的是 2 -> 3 +2. 当线程出栈 2 后,将 2 的 next 指针置空,所以 3 丢失 +3. 最后,线程 1 将 next 设置为新值,那结果肯定是 2 了 + +这个就是 ABA 问题,虽然 CAS 成功,但并不代表数据从获取到修改结束后,就一直没发生过改变,只能说明 CAS 执行原子操作的时候,数据是没变的。 + +解决上面代码示例中的 ABA 问题可以有很多,比如入栈的时候,不使用旧值引用,而是 new 一个新值。或者入栈的时候,不传递 Node 结点,而是传递 value,在或者不保存本地 next,而是在 compareAndSet 方法中,直接使用 node.next。 +这里,我们看看使用 AtomicStampedReference 如何解决这个问题。 + +#### 源码分析 + +AtomicStampedReference 使用版本号来控制 ABA 问题,就像数据库中使用乐观锁一样。 + +```java +package java.util.concurrent.atomic; + +public class AtomicStampedReference { + + // 定义一个 <引用,记录> 类,reference 则是实际的数据,stamp 则是这个数据的版本号 + 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; + + /** + * Creates a new {@code AtomicStampedReference} with the given + * initial values. + * + * 使用指定参数,初始化类。 + * + * @param initialRef the initial reference + * @param initialStamp the initial stamp + */ + public AtomicStampedReference(V initialRef, int initialStamp) { + pair = Pair.of(initialRef, initialStamp); + } + + /** + * Returns the current value of the reference. + * + * 获取当前的引用值 + * + * @return the current value of the reference + */ + public V getReference() { + return pair.reference; + } + + /** + * Returns the current value of the stamp. + * + * 获取当前引用值对应的记录数 + * + * @return the current value of the stamp + */ + public int getStamp() { + return pair.stamp; + } + + /** + * Returns the current values of both the reference and the stamp. + * Typical usage is {@code int[1] holder; ref = v.get(holder); }. + * + * 获取当前的引用值与记录数,这个记录数会放到参数数组中 + * + * @param stampHolder an array of size of at least one. On return, + * {@code stampholder[0]} will hold the value of the stamp. + * @return the current value of the reference + */ + public V get(int[] stampHolder) { + Pair pair = this.pair; + stampHolder[0] = pair.stamp; + return pair.reference; + } + + /** + * Atomically sets the value of both the reference and stamp + * to the given update values if the + * current reference is {@code ==} to the expected reference + * and the current stamp is equal to the expected stamp. + * + *

May fail + * spuriously and does not provide ordering guarantees, so is + * only rarely an appropriate alternative to {@code compareAndSet}. + * + * @param expectedReference the expected value of the reference + * @param newReference the new value for the reference + * @param expectedStamp the expected value of the stamp + * @param newStamp the new value for the stamp + * @return {@code true} if successful + */ + public boolean weakCompareAndSet(V expectedReference, + V newReference, + int expectedStamp, + int newStamp) { + return compareAndSet(expectedReference, newReference, + expectedStamp, newStamp); + } + + /** + * Atomically sets the value of both the reference and stamp + * to the given update values if the + * current reference is {@code ==} to the expected reference + * and the current stamp is equal to the expected stamp. + * + * @param expectedReference the expected value of the reference + * @param newReference the new value for the reference + * @param expectedStamp the expected value of the stamp + * @param newStamp the new value for the stamp + * @return {@code true} if successful + */ + //设置新值与新的记录数 + public boolean compareAndSet(V expectedReference, + V newReference, + int expectedStamp, + int newStamp) { + Pair current = pair; + //如果所有的当前值与期待值没有变化,且新数据等于旧数据,则直接返回 true + //如果所有的当前值与期待值没有变化,但数据要进行修改,则调用 casPair 方法,进行 CAS 修改操作 + return + expectedReference == current.reference && + expectedStamp == current.stamp && + ((newReference == current.reference && + newStamp == current.stamp) || + casPair(current, Pair.of(newReference, newStamp))); + } + + /** + * Unconditionally sets the value of both the reference and stamp. + * + * @param newReference the new value for the reference + * @param newStamp the new value for the stamp + */ + public void set(V newReference, int newStamp) { + Pair current = pair; + if (newReference != current.reference || newStamp != current.stamp) + this.pair = Pair.of(newReference, newStamp); + } + + /** + * Atomically sets the value of the stamp to the given update value + * if the current reference is {@code ==} to the expected + * reference. Any given invocation of this operation may fail + * (return {@code false}) spuriously, but repeated invocation + * when the current value holds the expected value and no other + * thread is also attempting to set the value will eventually + * succeed. + * + * @param expectedReference the expected value of the reference + * @param newStamp the new value for the stamp + * @return {@code true} if successful + */ + //只修改记录数 + public boolean attemptStamp(V expectedReference, int newStamp) { + Pair current = pair; + return + expectedReference == current.reference && + (newStamp == current.stamp || + casPair(current, Pair.of(expectedReference, newStamp))); + } + + // Unsafe mechanics CAS 操作 + + private static final sun.misc.Unsafe UNSAFE = sun.misc.Unsafe.getUnsafe(); + private static final long pairOffset = + objectFieldOffset(UNSAFE, "pair", AtomicStampedReference.class); + + //cmp 为期待值,val 为要替换的值 + private boolean casPair(Pair cmp, Pair val) { + //底层调用 UNSAFE 的 CAS 操作 + return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val); + } + + //获取 pair 成员变量在内存中的偏移量 + 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; + } + } +} +``` + +这个类其实很简单,还是利用 Unsafe 做 CAS 的操作,只不过在每次修改之前,验证了一下 stamp。 + +接下来,修改一开始测试 ABA 的代码,使用 AtomicStampedReference 的方式解决。 + +```java +import javax.annotation.Nullable; +import java.util.concurrent.atomic.AtomicStampedReference; + +public class OtherTest2 { + + private AtomicStampedReference stack = new AtomicStampedReference<>(null, 0); + + private void push(@Nullable Node newNode) { + if (newNode != null) { + Node node; + int stamp; + do { + int[] stampHolder = new int[1]; + node = stack.get(stampHolder); + stamp = stampHolder[0]; + + newNode.next = node; + } while (!stack.compareAndSet(node, newNode, stamp, stamp + 1)); + } + } + + private Node pop(boolean isSleep) { + Node node, next; + int stamp; + do { + int[] stampHolder = new int[1]; + node = stack.get(stampHolder); + stamp = stampHolder[0]; + + if (node == null) return null; + next = node.next; + try { + if (isSleep) Thread.sleep(1000); + } catch (Exception e) { + // nothing to do + } + } while (!stack.compareAndSet(node, next, stamp, stamp + 1)); + node.next = null; + return node; + } + + private void output() { + Node node = stack.get(new int[1]); + while (node != null) { + System.out.println(node.value); + node = node.next; + } + } + + private static class Node { + int value; + Node next; + + Node(int value) { + this.value = value; + } + } + + public static void main(String[] args) { + OtherTest2 ot = new OtherTest2(); + + ot.push(new Node(3)); + ot.push(new Node(2)); + ot.push(new Node(1)); + + new Thread(() -> ot.pop(true)).start(); + + new Thread(() -> { + Node node = ot.pop(false); + ot.pop(false); + ot.push(node); + }).start(); + + while (Thread.activeCount() > 1) { + Thread.yield(); + } + + ot.output(); + } +} +``` + +最后在测试,结果就是 3 了。 \ No newline at end of file diff --git a/second/week_02/81/Unsafe.md b/second/week_02/81/Unsafe.md new file mode 100644 index 0000000..52cac6d --- /dev/null +++ b/second/week_02/81/Unsafe.md @@ -0,0 +1,1140 @@ +#### 简述 + +Unsafe 并不是 JDK 的标准,它是 Sun 的内部实现,存在于 sun.misc 包中,在 Oracle 发行的 JDK 中并不包含其源代码。 + +虽然我们在一般的并发编程中不会直接用到 Unsafe,但是很多 Java 基础类库与诸如 Netty、Cassandra 和 Kafka 等高性能库都采用它,它在提升 Java 运行效率、增强 Java 语言底层操作能力方面起了很大作用。本文将深入到 Unsafe 的源码,分析一下它的逻辑。 + +本文使用 OpenJDK(jdk8-b120)中 Unsafe 的源码,Unsafe 的实现是和虚拟机实现相关的,不同的虚拟机实现,它们的对象结构可能不一样,这个 Unsafe 只能用于 Hotspot 虚拟机。 + +#### 源码分析 + +[源码查看](http://hg.openjdk.java.net/jdk/jdk/file/a1ee9743f4ee/jdk/src/share/classes/sun/misc/Unsafe.java) + +Unsafe 为调用者提供执行非安全操作的能力,由于返回的 Unsafe 对象可以读写任意的内存地址数据,调用者应该小心谨慎的使用该对象,一定不能把它传递到非信任代码。该类的大部分方法都是非常底层的操作,并牵涉到一小部分典型的机器都包含的硬件指令,编译器可以对这些进行优化。 + +```java +public final class Unsafe { + + private static native void registerNatives(); + static { + registerNatives(); + sun.reflect.Reflection.registerMethodsToFilter(Unsafe.class, "getUnsafe"); + } + + private Unsafe() {} + + private static final Unsafe theUnsafe = new Unsafe(); + + + @CallerSensitive + public static Unsafe getUnsafe() { + Class caller = Reflection.getCallerClass(); + if (!VM.isSystemDomainLoader(caller.getClassLoader())) + throw new SecurityException("Unsafe"); + return theUnsafe; + } + // ...... +} +``` + +上面的代码包含如下功能: + ++ 本地静态方法:registerNatives(),该方法会在静态块中执行 ++ 私有构造函数:该类实例是单例的,不能实例化,可以通过 getUnsafe() 方法获取实例 ++ 静态单例方法:getUnsafe(),获取实例 ++ 静态块:包含初始化的注册功能 + +要使用此类必须获得其实例,获得实例的方法是 getUnsafe(),那就先看看这个方法。 + +getUnsafe() 方法包含一个注释 @CallerSensitive,说明该方法不是谁都可以调用的。如果调用者不是由系统类加载器(bootstrap classloader)加载,则将抛出 SecurityException,所以默认情况下,应用代码调用此方法将抛出异常。我们的代码要想通过 getUnsafe() 获取实例是不可能的了,不过可通过反射获取 Unsafe 实例: + +```java +Field f= Unsafe.class.getDeclaredField("theUnsafe"); +f.setAccessible(true); +U= (Unsafe) f.get(null); +``` + +此处通过反射获取类的静态字段,这样就绕过了 getUnsafe() 的安全限制。 + +也可以通过反射获取构造方法再实例化,但这样违法了该类单例的原则,并且在使用上可能会有其它问题,所以不建议这样做。 + +再来看看如何获取指定变量的值: + +```java +public native int getInt(Object o, long offset); +``` + +获取指定对象中指定偏移量的字段或数组元素的值,参数 o 是变量关联的 Java 堆对象,如果指定的对象为 null,则获取内存中该地址的值(即 offset 为内存绝对地址)。 + +如果不符合以下任意条件,则该方法返回的值是不确定的: + ++ offset 通过 objectFieldOffset 方法获取的类的某一字段的偏移量,并且关联的对象 o 是该类的兼容对象(对象 o 所在的类必须是该类或该类的子类) ++ offset 通过 staticFieldOffset 方法获取的类的某一字段的偏移量,o 是通过 staticFieldBase 方法获取的对象 ++ 如果 o 引用的是数组,则 offset 的值为 B+N*S,其中 N 是数组的合法下标,B 是通过 arrayBaseOffset 方法从该数组类获取的,S 是通过 arrayIndexScale 方法从该数组类获取的 + +如果以上任意条件符合,则调用者能获取 Java 字段的引用,但是如果该字段的类型和该方法返回的类型不一致,则结果是不一定的,比如该字段是 short,但调用了 getInt 方法。 + +该方法通过两个参数引用一个变量,它为 Java 变量提供 double-register 地址模型。如果引用的对象为 null,则该方法将 offset 当作内存绝对地址,就像 getInt(long)一样,它为非 Java 变量提供 single-register 地址模型,然而 Java 变量的内存布局可能和非 Java 变量的内存布局不同,不应该假设这两种地址模型是相等的。同时,应该记住 double-register 地址模型的偏移量不应该和 single-register 地址模型中的地址(long 参数)混淆。 + +再看条件中提到的几个相关方法: + +```java +public native long objectFieldOffset(Field f); + +public native Object staticFieldBase(Field f); + +public native long staticFieldOffset(Field f); + +public native int arrayBaseOffset(Class arrayClass); + +public native int arrayIndexScale(Class arrayClass); +``` + +这几个方法分别是获取静态字段、非静态字段与数组字段的一些信息。 + +**objectFieldOffset** + +很难想象 JVM 需要使用这么多比特位来编码非数组对象的偏移量,为了和该类的其它方法保持一致,所以该方法也返回 long 类型。 + +**staticFieldBase** + +获取指定静态字段的位置,和 staticFieldOffset 一起使用。获取该静态字段所在的“对象”可通过类似 getInt(Object,long)的方法访问。 + +**staticFieldOffset** + +返回给定的字段在该类的偏移地址。对于任何给定的字段,该方法总是返回相同的值,同一个类的不同字段总是返回不同的值。从 1.4.1 开始,字段的偏移以 long 表示,虽然 Sun 的 JVM 只使用了 32 位,但是那些将静态字段存储到绝对地址的 JVM 实现需要使用 long 类型的偏移量,通过 getXX(null,long) 获取字段值,为了保持代码迁移到 64 位平台上 JVM 的优良性,必须保持静态字段偏移量的所有比。 + +**arrayBaseOffset** + +返回给定数组类第一个元素在内存中的偏移量。如果 arrayIndexScale 方法返回非 0 值,要获得访问数组元素的新的偏移量,则需要使用 s。 + +**arrayIndexScale** + +返回给定数组类的每个元素在内存中的 scale(所占用的字节)。然而对于“narrow”类型的数组,类似 getByte(Object, int)的访问方法一般不会获得正确的结果,所以这些类返回的 scale 会是 0。 + +下边用代码解释: + +```java +public class MyObj { + int objField=10; + static int clsField=10; + int[] array={10,20,30,40,50}; + static Unsafe U; + static { + try { + init(); + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + } + public static void init() throws NoSuchFieldException, IllegalAccessException { + Field f= Unsafe.class.getDeclaredField("theUnsafe"); + f.setAccessible(true); + U= (Unsafe) f.get(null); + } + //...... +} +``` + +定义一个类包含成员变量 objField、类变量 clsField、成员数组 array 用于实验。要获取正确的结果,必须满足注释里的三个条件之一: + +1、offset 通过 objectFieldOffset 方法获取的类的某一字段的偏移量,并且关联的对象 o 是该类的兼容对象(对象 o 所在的类必须是该类或该类的子类) + +```java +public class MyObjChild extends MyObj { + int anthor; +} +``` + +```java + static void getObjFieldVal() throws NoSuchFieldException { + Field field=MyObj.class.getDeclaredField("objField"); + long offset= U.objectFieldOffset(field); + MyObj obj=new MyObj(); + + int val= U.getInt(obj,offset); + System.out.println("1.\t"+(val==10)); + + MyObjChild child=new MyObjChild(); + int corVal1= U.getInt(child,offset); + System.out.println("2.\t"+(corVal1==10)); + + Field fieldChild=MyObjChild.class.getDeclaredField("objField"); + long offsetChild= U.objectFieldOffset(fieldChild); + System.out.println("3.\t"+(offset==offsetChild)); + int corVal2= U.getInt(obj,offsetChild); + System.out.println("4.\t"+(corVal2==10)); + + short errVal1=U.getShort(obj,offset); + System.out.println("5.\t"+(errVal1==10)); + + int errVal2=U.getInt("abcd",offset); + System.out.println("6.\t"+errVal2); + } +``` + +输出结果为: + +```java +true +true +true +true +true +-223271518 +``` + ++ 第一个参数 o 和 offset 都是从 MyObj 获取的,所以返回 true。 ++ 第二个参数 o 是 MyObjChild 的实例,MyObjChild 是 MyObj 的子类,对象 o 是 MyObj 的兼容实例,所以返回 true。这从侧面说明在虚拟机中子类的实例的内存结构继承了父类的实例的内存结构。 ++ 第三个比较子类和父类中获取的字段偏移量是否相同,返回 true 说明是一样的,既然是一样的,第四个自然就返回 true。 + +这里重点说一下第五个,objField 是一个 int 类型,占四个字节,其值为 10,二进制为 00000000 00000000 00000000 00001010。Intel 处理器读取内存使用的是小端(Little-Endian)模式,在使用 Intel 处理器的机器的内存中多字节类型是按小端存储的,即低位在内存的低字节存储,高位在内存的高字节存储,所以 int 10 在内存中是(offset 0-3) 00001010 00000000 00000000 00000000。使用 getShort 会读取两个字节,即 00001010 00000000,获取的值仍为 10。 + +但是某些处理器是使用大端(Big-Endian),如 ARM 支持小端和大端,使用此处理器的机器的内存就会按大端存储多字节类型,与小端相反,此模式下低位在内存的高字节存储,高位在内存的低字节存储,所以 int 10 在内存中是(offset 0-3)00000000 00000000 00000000 00001010。在这种情况下,getShort 获取的值将会是 0。 + +不同的机器可能产生不一样的结果,基于此情况,如果字段是 int 类型,但需要一个 short 类型,也不应该调用 getShort,而应该调用 getInt,然后强制转换成 short。此外,如果调用 getLong,该方法返回的值一定不是 10。就像方法注释所说,调用该类型方法时,要保证方法的返回值和字段的值是同一种类型。 + +第六个测试获取非 MyObj 实例的偏移位置的值,这种情况下代码本身并不会报错,但获取到的值并非该字段的值(未定义的值) + +2、offset 通过 staticFieldOffset 方法获取的类的某一字段的偏移量,o 是通过 staticFieldBase 方法获取的对象 + +```java + static void getClsFieldVal() throws NoSuchFieldException { + Field field=MyObj.class.getDeclaredField("clsField"); + long offset= U.staticFieldOffset(field); + Object obj=U.staticFieldBase(field); + int val1=U.getInt(MyObj.class,offset); + System.out.println("1.\t"+(val1==10)); + int val2=U.getInt(obj,offset); + System.out.println("2.\t"+(val2==10)); + } +``` + +输出结果: + +```java +true +true +``` + +获取静态字段的值,有两个方法:staticFieldBase 获取字段所在的对象,静态字段附着于 Class 本身(java.lang.Class 的实例),该方法返回的其实就是该类本身,本例中是 MyObj.class。 + +3、如果 o 引用的是数组,则 offset 的值为 B+N*S,其中 N 是数组的合法的下标,B 是通过 arrayBaseOffset 方法从该数组类获取的,S 是通过 arrayIndexScale 方法从该数组类获取的 + +```java + static void getArrayVal(int index,int expectedVal) throws NoSuchFieldException { + int base=U.arrayBaseOffset(int[].class); + int scale=U.arrayIndexScale(int[].class); + MyObj obj=new MyObj(); + Field field=MyObj.class.getDeclaredField("array"); + long offset= U.objectFieldOffset(field); + int[] array= (int[]) U.getObject(obj,offset); + int val1=U.getInt(array,(long)base+index*scale); + System.out.println("1.\t"+(val1==expectedVal)); + int val2=U.getInt(obj.array,(long)base+index*scale); + System.out.println("2.\t"+(val2==expectedVal)); + } +``` + +```java +getArrayVal(2,30); +``` +输出结果: + +```java +true +true +``` + +获取数组的值以及获取数组中某下标的值。获取数组某一下标的偏移量有一个计算公式 B+N*S,B 是数组元素在数组中的基准偏移量,S 是每个元素占用的字节数,N 是数组元素的下标。 + +有个要注意的地方,上面例子中方法内的数组的 offset 和 base 是两个完全不同的偏移量,offset 是数组 array 在对象 obj 中的偏移量,base 是数组元素在数组中的基准偏移量,这两个值没有任何联系,不能通过 offset 推导出 base。 + +getInt 的参数 o 可以是 null,在这种情况下,其和方法 getInt(long) 就是一样的了,offset 就不是表示相对的偏移地址了,而是表示内存中的绝对地址。操作系统中,一个进程是不能访问其他进程的内存的,所以传入 getInt 中的绝对地址必须是当前 JVM 管理的内存地址,否则进程会退出。 + +下一个方法,将值存储到 Java 变量中: + +```java +public native void putInt(Object o, long offset, int x); +``` + ++ 前两个参数会被解释成 Java 变量(字段或数组)的引用 ++ 参数给定的值会被存储到该变量,变量的类型必须和方法参数的类型一致 ++ 参数 o 是变量关联的 Java 堆对象,可以为 null ++ 参数 offset 代表该变量在该对象的位置,如果 o 是 null 则是内存的绝对地址 + +修改指定位置的内存,测试代码: + +```java + static void setObjFieldVal(int val) throws NoSuchFieldException { + Field field=MyObj.class.getDeclaredField("objField"); + long offset= U.objectFieldOffset(field); + MyObj obj=new MyObj(); + U.putInt(obj,offset,val); + int getVal= U.getInt(obj,offset); + System.out.println(val==getVal); + U.putLong(obj,offset,val); + Field fieldArray=MyObj.class.getDeclaredField("array"); + long offsetArray= U.objectFieldOffset(fieldArray); +// int[] array= (int[]) U.getObject(obj,offsetArray); +// for(int i=0;i c) { + Field[] fields = c.getDeclaredFields(); + for (int i = 0; i < fields.length; i++) { + if (Modifier.isStatic(fields[i].getModifiers())) { + return staticFieldBase(fields[i]); + } + } + return null; + } + + /** + * 返回给定的字段在该类的偏移地址 + * + * 对于任何给定的字段,该方法总是返回相同的值;同一个类的不同字段总是返回不同的值 + * + * 从 1.4.1 开始,字段的偏移以 long 表示,虽然 Sun 的 JVM 只使用了 32 位,但是那些将静态字段存储到绝对地址的 JVM 实现 + * 需要使用 long 类型的偏移量,通过 getXX(null,long) 获取字段值,为了保持代码迁移到 64 位平台上 JVM 的优良性, + * 必须保持静态字段偏移量的所有比特位 + */ + public native long staticFieldOffset(Field f); + + /** + * 很难想象 JVM 需要使用这么多比特位来编码非数组对象的偏移量,它们只需要很少的比特位就可以了(有谁看过有100个成员变量的类么? + * 一个字节能表示 256 个成员变量), + * 为了和该类的其他方法保持一致,所以该方法也返回 long 类型 + * + */ + public native long objectFieldOffset(Field f); + + /** + * 获取指定静态字段的位置,和 staticFieldOffset 一起使用 + * 获取该静态字段所在的"对象",这个"对象"可通过类似 getInt(Object,long) 的方法访问 + * 该"对象"可能是 null,并且引用的可能是对象的"cookie"(此处cookie具体含义未知,没有找到相关资料),不保证是真正的对象,该"对象"只能当作此类中 put 和 get 方法的参数, + * 其他情况下不应该使用它 + */ + public native Object staticFieldBase(Field f); + + /** + * 检查给定的类是否需要初始化,它通常和 staticFieldBase 方法一起使用 + * 只有当 ensureClassInitialized 方法不产生任何影响时才会返回 false + */ + public native boolean shouldBeInitialized(Class c); + + /** + * 确保给定的类已被初始化,它通常和 staticFieldBase 方法一起使用 + */ + public native void ensureClassInitialized(Class c); + + /** + * 返回给定数组类第一个元素在内存中的偏移量,如果 arrayIndexScale 方法返回非0值,要获得访问数组元素的新的偏移量, + * 需要使用 scale + */ + public native int arrayBaseOffset(Class arrayClass); + + /** The value of {@code arrayBaseOffset(boolean[].class)} */ + public static final int ARRAY_BOOLEAN_BASE_OFFSET + = theUnsafe.arrayBaseOffset(boolean[].class); + + /** The value of {@code arrayBaseOffset(byte[].class)} */ + public static final int ARRAY_BYTE_BASE_OFFSET + = theUnsafe.arrayBaseOffset(byte[].class); + + /** The value of {@code arrayBaseOffset(short[].class)} */ + public static final int ARRAY_SHORT_BASE_OFFSET + = theUnsafe.arrayBaseOffset(short[].class); + + /** The value of {@code arrayBaseOffset(char[].class)} */ + public static final int ARRAY_CHAR_BASE_OFFSET + = theUnsafe.arrayBaseOffset(char[].class); + + /** The value of {@code arrayBaseOffset(int[].class)} */ + public static final int ARRAY_INT_BASE_OFFSET + = theUnsafe.arrayBaseOffset(int[].class); + + /** The value of {@code arrayBaseOffset(long[].class)} */ + public static final int ARRAY_LONG_BASE_OFFSET + = theUnsafe.arrayBaseOffset(long[].class); + + /** The value of {@code arrayBaseOffset(float[].class)} */ + public static final int ARRAY_FLOAT_BASE_OFFSET + = theUnsafe.arrayBaseOffset(float[].class); + + /** The value of {@code arrayBaseOffset(double[].class)} */ + public static final int ARRAY_DOUBLE_BASE_OFFSET + = theUnsafe.arrayBaseOffset(double[].class); + + /** The value of {@code arrayBaseOffset(Object[].class)} */ + public static final int ARRAY_OBJECT_BASE_OFFSET + = theUnsafe.arrayBaseOffset(Object[].class); + + /** + * 返回给定数组类的每个元素在内存中的 scale(所占用的字节)。然而对于"narrow"类型的数组,类似 getByte(Object, int) 的访问方法 + * 一般不会获得正确的结果,所以这些类返回的 scale 会是 0 + * (本人水平有限,此处narrow类型不知道具体含义,不了解什么时候此方法会返回0) + */ + public native int arrayIndexScale(Class arrayClass); + + /** The value of {@code arrayIndexScale(boolean[].class)} */ + public static final int ARRAY_BOOLEAN_INDEX_SCALE + = theUnsafe.arrayIndexScale(boolean[].class); + + /** The value of {@code arrayIndexScale(byte[].class)} */ + public static final int ARRAY_BYTE_INDEX_SCALE + = theUnsafe.arrayIndexScale(byte[].class); + + /** The value of {@code arrayIndexScale(short[].class)} */ + public static final int ARRAY_SHORT_INDEX_SCALE + = theUnsafe.arrayIndexScale(short[].class); + + /** The value of {@code arrayIndexScale(char[].class)} */ + public static final int ARRAY_CHAR_INDEX_SCALE + = theUnsafe.arrayIndexScale(char[].class); + + /** The value of {@code arrayIndexScale(int[].class)} */ + public static final int ARRAY_INT_INDEX_SCALE + = theUnsafe.arrayIndexScale(int[].class); + + /** The value of {@code arrayIndexScale(long[].class)} */ + public static final int ARRAY_LONG_INDEX_SCALE + = theUnsafe.arrayIndexScale(long[].class); + + /** The value of {@code arrayIndexScale(float[].class)} */ + public static final int ARRAY_FLOAT_INDEX_SCALE + = theUnsafe.arrayIndexScale(float[].class); + + /** The value of {@code arrayIndexScale(double[].class)} */ + public static final int ARRAY_DOUBLE_INDEX_SCALE + = theUnsafe.arrayIndexScale(double[].class); + + /** The value of {@code arrayIndexScale(Object[].class)} */ + public static final int ARRAY_OBJECT_INDEX_SCALE + = theUnsafe.arrayIndexScale(Object[].class); +``` + +上面的一些方法之前已经提到过,注释也说的比较明白, 说一下 shouldBeInitialized 和 ensureClassInitialized,shouldBeInitialized 判断类是否已初始化,ensureClassInitialized 执行初始化。有个概念需要了解,虚拟机加载类包括加载和链接阶段,加载阶段只是把类加载进内存,链接阶段会验证加载的代码的合法性,并初始化静态字段和静态块;shouldBeInitialized 就是检查链接阶段有没有执行。 + +```java + static void clsInitialized() throws NoSuchFieldException { + System.out.println(U.shouldBeInitialized(MyObj.class)); + System.out.println(U.shouldBeInitialized(MyObjChild.class)); + U.ensureClassInitialized(MyObjChild.class); + System.out.println(U.shouldBeInitialized(MyObjChild.class)); + } +``` + +```java +public class MyObjChild extends MyObj { + static int f1=1; + final static int f2=1; + static { + f1=2; + System.out.println("MyObjChild init"); + } +} +``` + +输出: + +```java +false +true +MyObjChild init +false +``` + +第一行输出 false 是因为我这个代码(包括 main 方法)是在 MyObj 类里写的,执行 main 的时候,MyObj 已经加载并初始化了。调用 U.shouldBeInitialized(MyObjChild.class) 只会加载 MyObjChild.class,但不会初始化,执行 ensureClassInitialized 才会初始化。 + +```java + static void clsInitialized2() throws NoSuchFieldException { + Field f1=MyObjChild.class.getDeclaredField("f1"); + Field f2=MyObjChild.class.getDeclaredField("f2"); + long f1Offset= U.staticFieldOffset(f1); + long f2Offset= U.staticFieldOffset(f2); + int f1Val=U.getInt(MyObjChild.class,f1Offset); + int f2Val=U.getInt(MyObjChild.class,f2Offset); + System.out.println("1.\t"+(f1Val==0)); + System.out.println("2.\t"+(f2Val==1)); + U.ensureClassInitialized(MyObjChild.class); + f1Val=U.getInt(MyObjChild.class,f1Offset); + System.out.println("3.\t"+(f1Val==2)); + } +``` + +输出: + +```java +1.true +2.true MyObjChild init +3.true +``` + +f1 是 static int,f2 是 final static int,因为 f2 是 final,它的值在编译期就决定了,存放在类的常量表里,所以即使还没有初始化它的值就是 1。 + +```java + /** + * 获取本地指针所占用的字节大小,值为 4 或者 8。其他基本类型的大小由其内容决定 + */ + public native int addressSize(); + + /** The value of {@code addressSize()} */ + public static final int ADDRESS_SIZE = theUnsafe.addressSize(); + + /** + * 本地内存页大小,值为 2 的 N 次方 + */ + public native int pageSize(); +``` + +addressSize 返回指针的大小,32 位虚拟机返回 4,64 位虚拟机默认返回 8,开启指针压缩功能(-XX:-UseCompressedOops)则返回 4。基本类型不是用指针表示的,它是直接存储的值。一般情况下,我们会说在 Java 中,基本类型是值传递,对象是引用传递。Java 官方的表述是在任何情况下 Java 都是值传递。基本类型是传递值本身,对象类型是传递指针的值。 + +```java + /// random trusted operations from JNI: + /// JNI信任的操作 + /** + * 告诉虚拟机定义一个类,加载类不做安全检查,默认情况下,参数类加载器(ClassLoader)和保护域(ProtectionDomain)来自调用者类 + */ + public native Class defineClass(String name, byte[] b, int off, int len, + ClassLoader loader, + ProtectionDomain protectionDomain); + + /** + * 定义一个匿名类,这里说的和我们代码里写的匿名内部类不是一个东西。 + * (可以参考知乎上的一个问答 https://www.zhihu.com/question/51132462) + */ + public native Class defineAnonymousClass(Class hostClass, byte[] data, Object[] cpPatches); + + + /** + * 分配实例的内存空间,但不会执行构造函数。如果没有执行初始化,则会执行初始化 + */ + public native Object allocateInstance(Class cls) + throws InstantiationException; + + /** Lock the object. It must get unlocked via {@link #monitorExit}. + * + * 获取对象内置锁(即 synchronized 关键字获取的锁),必须通过 monitorExit 方法释放锁 + * (synchronized 代码块在编译后会产生两个指令:monitorenter,monitorexit) + */ + public native void monitorEnter(Object o); + + /** + * Unlock the object. It must have been locked via {@link + * #monitorEnter}. + * 释放锁 + */ + public native void monitorExit(Object o); + + /** + * 尝试获取对象内置锁,通过返回 true 和 false 表示是否成功获取锁 + */ + public native boolean tryMonitorEnter(Object o); + + /** Throw the exception without telling the verifier. + * 不通知验证器(verifier)直接抛出异常(此处 verifier 具体含义未知,没有找到相关资料) + */ + public native void throwException(Throwable ee); +``` + +allocateInstance 方法的测试 + +```java +public class MyObjChild extends MyObj { + static int f1=1; + int f2=1; + static { + f1=2; + System.out.println("MyObjChild init"); + } + public MyObjChild(){ + f2=2; + System.out.println("run construct"); + } +} +``` + +```java + static void clsInitialized3() throws InstantiationException { + MyObjChild myObj= (MyObjChild) U.allocateInstance(MyObjChild.class); + System.out.println("1.\t"+(MyObjChild.f1==2)); + System.out.println("1.\t"+(myObj.f2==0)); + } +``` + +输出: + +```java +MyObjChild init +1.true +2.true +``` + +可以看到分配对象的时候只执行了类的初始化代码,没有执行构造函数。 + +来看看最重要的 CAS 方法 + +```java + /** + * Atomically update Java variable to x if it is currently + * holding expected. + * @return true if successful + * + * 如果变量的值为预期值,则更新变量的值,该操作为原子操作 + * 如果修改成功则返回true + */ + public final native boolean compareAndSwapObject(Object o, long offset, + Object expected, + Object x); + + /** + * Atomically update Java variable to x if it is currently + * holding expected. + * @return true if successful + */ + public final native boolean compareAndSwapInt(Object o, long offset, + int expected, + int x); + + /** + * Atomically update Java variable to x if it is currently + * holding expected. + * @return true if successful + */ + public final native boolean compareAndSwapLong(Object o, long offset, + long expected, + long x); +``` + +这几个方法应该是最常用的方法了,用于实现原子性的 CAS 操作,这些操作可以避免加锁,一般情况下,性能会更好, java.util.concurrent 包下很多类就是用的这些 CAS 操作而没有用锁。 + +```java + static void cas() throws NoSuchFieldException { + Field field=MyObj.class.getDeclaredField("objField"); + long offset= U.objectFieldOffset(field); + MyObj myObj=new MyObj(); + myObj.objField=1; + U.compareAndSwapInt(myObj,offset,0,2); + System.out.println("1.\t"+(myObj.objField==2)); + U.compareAndSwapInt(myObj,offset,1,2); + System.out.println("2.\t"+(myObj.objField==2)); + } +``` + +输出: + +```java +1.false +2.true +``` + +```java + /** + * 获取给定变量的引用值,该操作有 volatile 加载语意,其他方面和 getObject(Object, long) 一样 + */ + public native Object getObjectVolatile(Object o, long offset); + + /** + * 将引用值写入给定的变量,该操作有 volatile 加载语意,其他方面和 putObject(Object, long, Object) 一样 + */ + public native void putObjectVolatile(Object o, long offset, Object x); + + /** Volatile version of {@link #getInt(Object, long)} */ + public native int getIntVolatile(Object o, long offset); + + /** Volatile version of {@link #putInt(Object, long, int)} */ + public native void putIntVolatile(Object o, long offset, int x); + + /** Volatile version of {@link #getBoolean(Object, long)} */ + public native boolean getBooleanVolatile(Object o, long offset); + + /** Volatile version of {@link #putBoolean(Object, long, boolean)} */ + public native void putBooleanVolatile(Object o, long offset, boolean x); + + /** Volatile version of {@link #getByte(Object, long)} */ + public native byte getByteVolatile(Object o, long offset); + + /** Volatile version of {@link #putByte(Object, long, byte)} */ + public native void putByteVolatile(Object o, long offset, byte x); + + /** Volatile version of {@link #getShort(Object, long)} */ + public native short getShortVolatile(Object o, long offset); + + /** Volatile version of {@link #putShort(Object, long, short)} */ + public native void putShortVolatile(Object o, long offset, short x); + + /** Volatile version of {@link #getChar(Object, long)} */ + public native char getCharVolatile(Object o, long offset); + + /** Volatile version of {@link #putChar(Object, long, char)} */ + public native void putCharVolatile(Object o, long offset, char x); + + /** Volatile version of {@link #getLong(Object, long)} */ + public native long getLongVolatile(Object o, long offset); + + /** Volatile version of {@link #putLong(Object, long, long)} */ + public native void putLongVolatile(Object o, long offset, long x); + + /** Volatile version of {@link #getFloat(Object, long)} */ + public native float getFloatVolatile(Object o, long offset); + + /** Volatile version of {@link #putFloat(Object, long, float)} */ + public native void putFloatVolatile(Object o, long offset, float x); + + /** Volatile version of {@link #getDouble(Object, long)} */ + public native double getDoubleVolatile(Object o, long offset); + + /** Volatile version of {@link #putDouble(Object, long, double)} */ + public native void putDoubleVolatile(Object o, long offset, double x); + +``` + +这是具有 volatile 语意的 get 和 put方法。volatile 语意为保证不同线程之间的可见行,即一个线程修改一个变量之后,保证另一线程能观测到此修改。这些方法可以使非 volatile 变量具有 volatile 语意。 + +```java + /** + * putObjectVolatile(Object, long, Object)的另一个版本(有序的/延迟的),它不保证其他线程能立即看到修改, + * 该方法通常只对底层为 volatile 的变量(或者 volatile 类型的数组元素)有帮助 + */ + public native void putOrderedObject(Object o, long offset, Object x); + + /** Ordered/Lazy version of {@link #putIntVolatile(Object, long, int)} */ + public native void putOrderedInt(Object o, long offset, int x); + + /** Ordered/Lazy version of {@link #putLongVolatile(Object, long, long)} */ + public native void putOrderedLong(Object o, long offset, long x); +``` + +有三类很相近的方法:putXx、putXxVolatile 与 putOrderedXx: + ++ putXx 只是写本线程缓存,不会将其它线程缓存置为失效,所以不能保证其它线程一定看到此次修改; ++ putXxVolatile 相反,它可以保证其它线程一定看到此次修改; ++ putOrderedXx 也不保证其它线程一定看到此次修改,但和 putXx 又有区别,它的注释上有两个关键字:顺序性(Ordered)和延迟性(lazy),顺序性是指不会发生重排序,延迟性是指其它线程不会立即看到此次修改,只有当调用 putXxVolatile 使才能看到。 + +```java + /** + * 释放当前阻塞的线程。如果当前线程没有阻塞,则下一次调用 park 不会阻塞。这个操作是"非安全"的 + * 是因为调用者必须通过某种方式保证该线程没有被销毁 + * + */ + public native void unpark(Object thread); + + /** + * 阻塞当前线程,当发生如下情况时返回: + * 1、调用 unpark 方法 + * 2、线程被中断 + * 3、时间过期 + * 4、spuriously + * 该操作放在 Unsafe 类里没有其它意义,它可以放在其它的任何地方 + */ + public native void park(boolean isAbsolute, long time); +``` + +阻塞和释放当前线程,java.util.concurrent 中的锁就是通过这两个方法实现线程阻塞和释放的。 + +```java + /** + *获取一段时间内,运行的任务队列分配到可用处理器的平均数(平常说的 CPU 使用率) + * + */ + public native int getLoadAverage(double[] loadavg, int nelems); +``` + +统计 CPU 负载。 + +```java + // The following contain CAS-based Java implementations used on + // platforms not supporting native instructions + //下面的方法包含基于 CAS 的 Java 实现,用于不支持本地指令的平台 + /** + * 在给定的字段或数组元素的当前值原子性的增加给定的值 + * @param o 字段/元素所在的对象/数组 + * @param offset 字段/元素的偏移 + * @param delta 需要增加的值 + * @return 原值 + * @since 1.8 + */ + public final int getAndAddInt(Object o, long offset, int delta) { + int v; + do { + v = getIntVolatile(o, offset); + } while (!compareAndSwapInt(o, offset, v, v + delta)); + return v; + } + + 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; + } + + /** + * 将给定的字段或数组元素的当前值原子性的替换给定的值 + * @param o 字段/元素所在的对象/数组 + * @param offset field/element offset + * @param newValue 新值 + * @return 原值 + * @since 1.8 + */ + //举个例子,比如 i++ 这个操作,它不是原子性的,而是分为两步: + //1.先获取 i 的值 + //2.对 i 加 1 + //所以为了保证原子性,CAS 在第二部操作的时候,会先判断当前要修改的值,是否等于预期值: + //1.如果等于则直接修改 + //2.如果不等于,则重新拿一个新的预期值,在和要修改的当前值比较(只有符合预期值,才能修改,否则说明已经被其他线程修改,那么当前线程需要重新拿一次属于自己的预期值)。 + 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 的一些原子操作实现,也是比较常用的方法。 + +```java + //确保该栏杆前的读操作不会和栏杆后的读写操作发生重排序 + + public native void loadFence(); + + //确保该栏杆前的写操作不会和栏杆后的读写操作发生重排序 + + public native void storeFence(); + + //确保该栏杆前的读写操作不会和栏杆后的读写操作发生重排序 + + public native void fullFence(); + + //抛出非法访问错误,仅用于VM内部 + + private static void throwIllegalAccessError() { + throw new IllegalAccessError(); + } +``` + +这是实现内存屏障的几个方法,类似于 volatile 的语意,保证内存可见性和禁止重排序。这几个方法涉及到 JMM(Java 内存模型),有兴趣的可参考Java [内存模型 Cookbook 翻译](http://ifeve.com/jmm-cookbook-mb/) 。 + +#### 一些补充 + +1. JDK11包含了Unsafe源码,内部实现转移到jdk.internal.misc这个包里,仍然属于半开放状态,提供的方法可能随时修改. +2. JDK11的Unsafe大部分方法都带@HotSpotIntrinsicCandidate注解,说明会被直接映射成机器指令,而不是方法调用. 还有些方法用了@ForceInline注解,能保证被内联优化. +3. 文中提到的几个@Deprecated方法在JDK11里不是删除了就是原型改变了(主要是offset变成了long类型) +4. "narrow类型"估计是指数组中的每个元素占用不到1个字节的情况, 目前所见的JDK实现没有这种情况, 估计有的JDK把boolean数组中的每个元素只用1 bit来实现的. +5. "Java 官方的表述是在任何情况下 Java 都是值传递。基本类型是传递值本身,对象类型是传递指针的值。"我完全赞同这种说法,非常清晰准确,我觉得严格的"引用"含义应该像C++中的"&"和C#中的"ref/out"这种变量本身的别名(就是说Java对象的引用应该是指针的指针). +6. "不通知验证器(verifier)直接抛出异常"应该是指可以绕过throws声明. \ No newline at end of file -- Gitee