《Java并发编程之美》第 2 章 并发编程的其他基础知识 阅读笔记 3

《Java并发编程之美》第 2 章 并发编程的其他基础知识 阅读笔记 3

第 2 章 并发编程的其他基础知识

2.1 什么是多线程并发编程

首先要澄清并发和并行的概念,

并发是指同一个时间段内多个任务同时都在执行,并且都没有执行结束,
而并行是说在单位时间内多个任务同时在执行。

并发任务强调在一个时间段内同时执行,而一个时间段由多个单位时间累积而成,所以说并发的多个任务在单位时间内不一定同时在执行 。

在单 CPU 的时代多个任务都是并发执行的,这是因为单个 CPU 同时只能执行一个任务。

在单 CPU 时代多任务是共享一个 CPU 的,当一个任务占用 CPU 运行时,其他任务就会被挂起,

当占用 CPU 的任务时间片用完后,会把 CPU 让给其他任务来使用,

所以在单 CPU 时代多线程编程是没有太大意义的,并且线程间频繁的上下文切换还会带来额外开销。

图 2-1 所示为在单个 CPU 上运行两个线程,线程 A 和线程 B 是轮流使用 CPU 进行任务处理的,也就是在某个时间内单个 CPU 只执行一个线程上面的任务。

当线程 A 的时间片用完后会进行线程上下文切换,也就是保存当前线程 A 的执行上下文,然后切换到线程 B 来占用 CPU 运行任务。

图 2-1

图 2-2 所示为双 CPU 配置,线程 A 和线程 B 各自在自己的 CPU 上执行任务,实现了真正的并行运行。

图 2-2

而在多线程编程实践中,线程的个数往往多于 CPU 的个数,所以一般都称多线程并发编程而不是多线程并行编程。

2.2 为什么要进行多线程并发编程

多核 CPU 时代的到来打破了单核 CPU 对多线程效能的限制。

多个 CPU 意味着每个线程可以使用自己的 CPU 运行,这减少了线程上下文切换的开销,

但随着对应用系统性能和吞吐量要求的提高,出现了处理海量数据和请求的要求,这些都对高并发编程有着迫切的需求。

2.3 Java 中的线程安全问题

谈到线程安全问题,我们先说说什么是共享资源。所谓共享资源,就是说该资源被多个线程所持有或者说多个线程都可以去访问该资源。

线程安全问题是指当多个线程同时读写一个共享资源并且没有任何同步措施时,导致出现脏数据或者其他不可预见的结果的问题,

如图 2-3 所示。

图 2-3

在图 2-3 中,线程 A 和线程 B 可以同时操作主内存中的共享变量,

那么线程安全问题和共享资源之间是什么关系呢?是不是说多个线程共享了资源,当它们都去访问这个共享资源时就会产生线程安全问题呢?

答案是否定的,如果多个线程都只是读取共享资源,而不去修改,那么就不会存在线程安全问题,

只有当至少一个线程修改共享资源时才会存在线程安全问题。

最典型的就是计数器类的实现,计数变量 count 本身是一个共享变量,多个线程可以对其进行递增操作,

如果不使用同步措施,由于递增操作是获取---计算---保存三步操作,因此可能导致计数不准确,如下所示。

t1 t2 t3 t4
线程 A 从内存读取 count 值到本线程 递增本线程内 count 的值 写回主内存
线程 B 从内存读取 count 值到本线程 递增本线程内 count 的值 写回主内存

假如当前 count=0,在 t1 时刻线程 A 读取 count 值到本地变量 countA。

然后在 t2 时刻递增 countA 的值为 1,同时线程 B 读取 count 的值 0 到本地变量 countB,此时 countB 的值为 0(因为 countA 的值还没有被写入主内存)​。

在 t3 时刻线程 A 才把 countA 的值 1 写入主内存,至此线程 A 一次计数完毕,同时线程 B 递增 CountB 的值为 1。

在 t4 时刻线程 B 把 countB 的值 1 写入内存,至此线程 B 一次计数完毕。

这里先不考虑内存可见性问题,明明是两次计数,为何最后结果是 1 而不是 2 呢?其实这就是共享变量的线程安全问题。

那么如何来解决这个问题呢?这就需要在线程访问共享变量时进行适当的同步,

在 Java 中最常见的是使用关键字 synchronized 进行同步,下面会有具体介绍。

2.4 Java 中共享变量的内存可见性问题

谈到内存可见性,我们首先来看看在多线程下处理共享变量时 Java 的内存模型,如图 2-4 所示。

图 2-4

Java 内存模型规定,将所有的变量都存放在主内存中,当线程使用变量时,会把主内存里面的变量复制到自己的工作空间或者叫作工作内存,线程读写变量时操作的是自己工作内存中的变量。

Java 内存模型是一个抽象的概念,那么在实际实现中线程的工作内存是什么呢?请看图 2-5

图 2-5

图中所示是一个双核 CPU 系统架构,每个核有自己的控制器和运算器,

其中控制器包含一组寄存器和操作控制器,

运算器执行算术逻辑运算。

每个核都有自己的一级缓存,在有些架构里面还有一个所有 CPU 都共享的二级缓存。

那么 Java 内存模型里面的工作内存,就对应这里的 L1 或者 L2 缓存或者 CPU 的寄存器。

当一个线程操作共享变量时,它首先从主内存复制共享变量到自己的工作内存,然后对工作内存里的变量进行处理,处理完后将变量值更新到主内存。

那么假如线程 A 和线程 B 同时处理一个共享变量,会出现什么情况?

我们使用图 2-5 所示 CPU 架构,假设线程 A 和线程 B 使用不同 CPU 执行,并且当前两级 Cache 都为空,

那么这时候由于 Cache 的存在,将会导致内存不可见问题,具体看下面的分析。

线程 A 首先获取共享变量 X 的值,由于两级 Cache 都没有命中,所以加载主内存中 X 的值,假如为 0。

然后把 X=0 的值缓存到两级缓存,线程 A 修改 X 的值为 1,然后将其写入两级 Cache,并且刷新到主内存。

线程 A 操作完毕后,线程 A 所在的 CPU 的两级 Cache 内和主内存里面的 X 的值都是 1。

线程 B 获取 X 的值,首先一级缓存没有命中,然后看二级缓存,二级缓存命中了,所以返回 X= 1;

到这里一切都是正常的,因为这时候主内存中也是 X=1。

然后线程 B 修改 X 的值为 2,并将其存放到线程 2 所在的一级 Cache 和共享二级 Cache 中,

最后更新主内存中X的值为 2;到这里一切都是好的。

线程 A 这次又需要修改 X 的值,获取时一级缓存命中,并且 X=1,到这里问题就出现了 ,

明明线程 B 已经把 X 的值修改为了 2,为何线程 A 获取的还是 1 呢?

这就是共享变量的内存不可见问题,也就是线程 B 写入的值对线程 A 不可见。

那么如何解决共享变量内存不可见问题?使用 Java 中的 volatile 关键字就可以解决这个问题,下面会有讲解。

2.5 Java 中的 synchronized 关键字

2.5.1 synchronized 关键字介绍

synchronized 块是 Java 提供的一种原子性内置锁,Java 中的每个对象都可以把它当作一个同步锁来使用,

这些 Java 内置的使用者看不到的锁被称为内部锁,也叫作监视器锁 。

线程的执行代码在进入 synchronized 代码块前会自动获取内部锁,这时候其他线程访问该同步代码块时会被阻塞挂起。

拿到内部锁的线程会在

正常退出同步代码块

或者抛出异常后

或者在同步块内调用了该内置锁资源的wait系列方法时释放该内置锁。

内置锁是排它锁,也就是当一个线程获取这个锁后,其他线程必须等待该线程释放锁后才能获取该锁。

另外,由于 Java 中的线程是与操作系统的原生线程一一对应的,

所以当阻塞一个线程时,需要从用户态切换到内核态执行阻塞操作,这是很耗时的操作,

而 synchronized 的使用就会导致上下文切换。

2.5.2 synchronized 的内存语义

前面介绍了共享变量内存可见性问题主要是由于线程的工作内存导致的,下面我们来讲解 synchronized 的一个内存语义,

这个内存语义就可以解决共享变量内存可见性问题 。

进入 synchronized 块的内存语义是把在 synchronized 块内使用到的变量从线程的工作内存中清除,

这样在 synchronized 块内使用到该变量时就不会从线程的工作内存中获取,而是直接从主内存中获取。

退出 synchronized 块的内存语义是把在 synchronized 块内对共享变量的修改刷新到主内存。

其实这也是加锁和释放锁的语义,

当获取锁后会清空锁块内本地内存中将会被用到的共享变量,在使用这些共享变量时从主内存进行加载,

在释放锁时将本地内存中修改的共享变量刷新到主内存。

除可以解决共享变量内存可见性问题外,synchronized 经常被用来实现原子性操作。

另外请注意,synchronized 关键字会引起线程上下文切换并带来线程调度开销。

2.6 Java 中的 volatile 关键字

上面介绍了使用锁的方式可以解决共享变量内存可见性问题,但是使用锁太笨重,因为它会带来线程上下文的切换开销。

对于解决内存可见性问题,Java 还提供了一种弱形式的同步,也就是使用 volatile 关键字。

该关键字可以确保对一个变量的更新对其他线程马上可见。

当一个变量被声明为 volatile 时,线程在写入变量时不会把值缓存在寄存器或者其他地方,而是会把值刷新回主内存。
当其他线程读取该共享变量时,会从主内存重新获取最新值,而不是使用当前线程的工作内存中的值。

volatile 的内存语义和 synchronized 有相似之处,具体来说就是,

当线程写入了 volatile 变量值时就等价于线程退出 synchronized 同步块(把写入工作内存的变量值同步到主内存)​,

读取 volatile 变量值时就相当于进入同步块(先清空本地内存变量值,再从主内存获取最新值)​。

下面看一个使用 volatile 关键字解决内存可见性问题的例子。如下代码中的共享变量 value 是线程不安全的,因为这里没有使用适当的同步措施。

java 复制代码
    public class ThreadNotSafeInteger {
        private int value;
        public int get() {
            return value;
        }
        public void set(int value) {
            this.value = value;
        }
    }

首先来看使用 synchronized 关键字进行同步的方式。

java 复制代码
    public class ThreadSafeInteger {
        private int value;
        public synchronized int get() {
            return value;
        }
        public synchronized  void set(int value) {
            this.value = value;
        }
    }

然后是使用 volatile 进行同步。

java 复制代码
    public class ThreadSafeInteger {
        private volatile int value;
        public int get() {
            return value;
        }
        public void set(int value) {
            this.value = value;
        }
    }

在这里使用 synchronized 和使用 volatile 是等价的,都解决了共享变量 value 的内存可见性问题,

但是前者是独占锁,同时只能有一个线程调用 get() 方法,其他调用线程会被阻塞,

同时会存在线程上下文切换和线程重新调度的开销,这也是使用锁方式不好的地方。

而后者是非阻塞算法,不会造成线程上下文切换的开销。

但并非在所有情况下使用它们都是等价的,volatile 虽然提供了可见性保证,但并不保证操作的原子性。

那么一般在什么时候才使用 volatile 关键字呢?

写入变量值不依赖变量的当前值时。因为如果依赖当前值,将是获取---计算---写入三步操作,这三步操作不是原子性的,而 volatile 不保证原子性。

读写变量值时没有加锁。因为加锁本身已经保证了内存可见性,这时候不需要把变量声明为 volatile 的。

2.7 Java 中的原子性操作

所谓原子性操作,是指执行一系列操作时,这些操作要么全部执行,要么全部不执行,不存在只执行其中一部分的情况。

在设计计数器时一般都先读取当前值,然后 +1,再更新。这个过程是读---改---写的过程,如果不能保证这个过程是原子性的,那么就会出现线程安全问题。

如下代码是线程不安全的,因为不能保证 ++value 是原子性操作。

java 复制代码
    public class ThreadNotSafeCount {
        private  Long value;
        public Long getCount() {
            return value;
        }
        public void inc() {
            ++value;
        }
    }

使用 Javap -c 命令查看汇编代码,如下所示。

java 复制代码
public void inc();
  Code:
    0: aload_0
    1: dup
    2: getfield      #2                  // Field value:J
    5: lconst_1
    6: ladd
    7: putfield      #2                  // Field value:J
    10: return

由此可见,简单的 ++value 由 2、5、6、7 四步组成,

其中第 2 步是获取当前value的值并放入栈顶,

第 5 步把常量 1 放入栈顶,

第 6 步把当前栈顶中两个值相加并把结果放入栈顶,

第 7 步则把栈顶的结果赋给 value 变量。

因此,Java 中简单的一句 ++value 被转换为汇编后就不具有原子性了。

javap -c 是 JDK 自带的反汇编命令,用来把一个 .class 文件里的方法体拆成 JVM 字节码指令。 它不是把字节码还原成 Java 源码,而是展示编译器生成的底层指令序列。

扩展 javap 命令

基本用法

bash 复制代码
javap -c ClassName
javap -c com.example.MyClass
javap -c MyClass.class

如果类不在当前目录,需要配合 -classpath:

bash 复制代码
javap -c -cp target/classes com.example.MyClass

一个简单例子

java 复制代码
public class Add {
    public static int add(int a, int b) {
        return a + b;
    }
}

执行:

bash 复制代码
javap -c Add

可能得到:

text 复制代码
public static int add(int, int);
  Code:
     0: iload_1
     1: iload_2
     2: iadd
     3: ireturn

含义大致是:

指令 含义
iload_1 把局部变量表中第 1 个槽位的 int 参数压入操作数栈
iload_2 把第 2 个槽位的 int 参数压入操作数栈
iadd 弹出栈顶两个 int 相加,再把结果压回栈顶
ireturn 返回栈顶的 int 值

常用组合

命令 作用
javap -c 只看方法的字节码指令
javap -p -c 包括 private 方法在内,全部反汇编
javap -v 输出常量池、行号表、局部变量表、栈帧信息等完整信息
javap -c -l 在字节码旁显示源码行号和局部变量表
javap -c -s 显示方法的内部类型签名

适用场景

  • 验证 for、if、switch、try-catch 等语法最终生成了什么字节码。
  • 分析 invokevirtual、invokespecial、invokestatic、invokeinterface、invokedynamic 等方法调用方式。
  • 理解自动装箱、字符串拼接、lambda、增强 for 循环等语法糖的实现。
  • 排查某些"看起来一样"的代码为什么性能不同。
  • 学习 JVM 基于栈的执行模型。

需要注意

javap -c 输出的是字节码指令,不是原始 Java 源码;如果想看更接近源码的反编译结果,需要用 IDEA 内置反编译器、CFR、JD-GUI 等工具。

那么如何才能保证多个操作的原子性呢?最简单的方法就是使用 synchronized 关键字进行同步,修改代码如下。

java 复制代码
    public class ThreadSafeCount {
        private  Long value;
        public synchronized Long getCount() {
            return value;
        }
        public synchronized void inc() {
            ++value;
        }
    }

使用 synchronized 关键字的确可以实现线程安全性,即内存可见性和原子性,

但是 synchronized 是独占锁,没有获取内部锁的线程会被阻塞掉,而这里的 getCount 方法只是读操作,多个线程同时调用不会存在线程安全问题。

但是加了关键字 synchronized 后,同一时间就只能有一个线程可以调用,这显然大大降低了并发性。

你也许会问,既然是只读操作,那为何不去掉 getCount 方法上的 synchronized 关键字呢?

其实是不能去掉的,别忘了这里要靠 synchronized 来实现 value 的内存可见性。

那么有没有更好的实现呢?答案是肯定的,

下面将讲到的在内部使用非阻塞 CAS 算法实现的原子性操作类 AtomicLong 就是一个不错的选择。

2.8 Java 中的 CAS 操作

在 Java 中,锁在并发处理中占据了一席之地,但是使用锁有一个不好的地方,就是当一个线程没有获取到锁时会被阻塞挂起,这会导致线程上下文的切换和重新调度开销。

Java 提供了非阻塞的 volatile 关键字来解决共享变量的可见性问题,这在一定程度上弥补了锁带来的开销问题,但是 volatile 只能保证共享变量的可见性,不能解决读---改---写等的原子性问题。

CAS 即 Compare and Swap 意思是比较并交换 ,其是 JDK 提供的非阻塞原子性操作,它通过硬件保证了比较---更新操作的原子性。

JDK 里面的 Unsafe 类提供了一系列的 compareAndSwap* 方法,下面以 compareAndSwapLong 方法为例进行简单介绍。

boolean compareAndSwapLong(Object obj, long valueOffset, long expect, long update)方法:

其中 compareAndSwap 的意思是比较并交换。

CAS 有四个操作数,分别为:对象内存位置、对象中的变量的偏移量、变量预期值和新的值。
其操作含义是,如果对象 obj 中内存偏移量为 valueOffset 的变量值为 expect,则使用新的值 update 替换旧的值 expect。这是处理器提供的一个原子性指令。

关于 CAS 操作有个经典的 ABA 问题,具体如下:

假如线程 I 使用 CAS 修改初始值为 A 的变量 X,那么线程 I 会首先去获取当前变量X的值(为 A)​,然后使用 CAS 操作尝试修改 X 的值为 B,如果使用 CAS 操作成功了,那么程序运行一定是正确的吗?其实未必,

这是因为有可能在线程 I 获取变量 X 的值 A 后,在执行 CAS 前,线程 II 使用 CAS 修改了变量 X 的值为 B,然后又使用 CAS 修改了变量 X 的值为 A。

所以虽然线程 I 执行 CAS 时 X 的值是 A,但是这个 A 已经不是线程 I 获取时的 A 了。这就是 ABA 问题。

ABA 问题的产生是因为变量的状态值产生了环形转换,就是变量的值可以从 A 到 B,然后再从 B 到 A。

如果变量的值只能朝着一个方向转换,比如 A 到 B, B 到 C,不构成环形,就不会存在问题。

JDK 中的 AtomicStampedReference 类给每个变量的状态值都配备了一个时间戳,从而避免了 ABA 问题的产生。

2.9 Unsafe 类

2.9.1 Unsafe 类中的重要方法

JDK 的 rt.jar 包中的 Unsafe 类提供了硬件级别的原子性操作,Unsafe 类中的方法都是 native 方法,它们使用 JNI 的方式访问本地 C++ 实现库。

下面我们来了解一下 Unsafe 提供的几个主要的方法以及编程时如何使用 Unsafe 类做一些事情。

long objectFieldOffset(Field field)方法:

返回指定的变量在所属类中的内存偏移地址,该偏移地址仅仅在该 Unsafe 函数中访问指定字段时使用。

如下代码使用 Unsafe 类获取变量 value 在 AtomicLong 对象中的内存偏移。

java 复制代码
    static {
    try {
        valueOffset = unsafe.objectFieldOffset
            (AtomicLong.class.getDeclaredField("value"));
    } catch (Exception ex) { throw new Error(ex); }
    }

int arrayBaseOffset(Class arrayClass)方法:获取数组中第一个元素的地址。

boolean compareAndSwapLong(Object obj, long offset, long expect, long update)方法:

比较对象 obj 中偏移量为 offset 的变量的值是否与 expect 相等,相等则使用 update 值更新,然后返回 true,否则返回 false。

public native long getLongvolatile(Object obj, long offset)方法:

获取对象 obj 中偏移量为 offset 的变量对应 volatile 语义的值。

void putLongvolatile(Object obj, long offset, long value)方法:

设置 obj 对象中 offset 偏移的类型为 long 的 field 的值为 value,支持 volatile 语义。

void putOrderedLong(Object obj, long offset, long value)方法:

设置 obj 对象中 offset 偏移地址对应的 long 型 field 的值为 value。

这是一个有延迟的 putLongvolatile 方法,并且不保证值修改对其他线程立刻可见。

只有在变量使用 volatile 修饰并且预计会被意外修改时才使用该方法。

void park(boolean isAbsolute, long time)方法:

阻塞当前线程,其中参数 isAbsolute 等于 false 且 time 等于 0 表示一直阻塞。

time 大于 0 表示等待指定的 time 后阻塞线程会被唤醒,这个 time 是个相对值,是个增量值,

也就是相对当前时间累加 time 后当前线程就会被唤醒。

如果 isAbsolute 等于 true,并且 time 大于 0,则表示阻塞的线程到指定的时间点后会被唤醒,

这里 time 是个绝对时间,是将某个时间点换算为 ms 后的值。

另外,当其他线程调用了当前阻塞线程的 interrupt 方法而中断了当前线程时,当前线程也会返回,

而当其他线程调用了 unPark 方法并且把当前线程作为参数时当前线程也会返回。

void unpark(Object thread)方法:

唤醒调用 park 后阻塞的线程。

下面是 JDK8 新增的函数,这里只列出 Long 类型操作。

long getAndSetLong(Object obj, long offset, long update)方法:

获取对象 obj 中偏移量为 offset 的变量 volatile 语义的当前值,并设置变量 volatile 语义的值为 update。

java 复制代码
    public final long getAndSetLong(Object obj, long offset, long update)
      {
        long l;
        do
        {
          l = getLongvolatile(obj, offset); //(1)
        } while (! compareAndSwapLong(obj, offset, l, update));
        return l;
      }

由以上代码可知,首先(1)处的getLongvolatile获取当前变量的值,然后使用CAS原子操作设置新值。这里使用while循环是考虑到,在多个线程同时调用的情况下CAS失败时需要重试。

long getAndAddLong(Object obj, long offset, long addValue)方法:

获取对象obj中偏移量为offset的变量volatile语义的当前值,并设置变量值为原始值+addValue。

java 复制代码
    public final long getAndAddLong(Object obj, long offset, long addValue)
    {
      long l;
      do
      {
        l = getLongvolatile(obj, offset);
      } while (! compareAndSwapLong(obj, offset, l, l + addValue));
      return l;
      }

类似getAndSetLong的实现,只是这里进行CAS操作时使用了原始值+传递的增量参数addValue的值。

2.9.2 如何使用 Unsafe 类

看到 Unsafe 这个类如此厉害,你肯定会忍不住试一下下面的代码,期望能够使用 Unsafe 做点事情。

java 复制代码
package com.yangjunbo.juc.two;

import sun.misc.Unsafe;

/**
 * ClassName: TestUnSafe
 * Package: com.yangjunbo.juc.two
 * Description:
 *
 * @Author 杨钧博
 * @Create 2026/10/20 15:28
 * @Version 1.0
 */
public class TestUnSafe {
    //获取Unsafe的实例(2.2.1)
    static final Unsafe unsafe = Unsafe.getUnsafe();
    //记录变量state在类TestUnSafe中的偏移值(2.2.2)
    static final long stateOffset;
    //变量(2.2.3)
    private volatile long state=0;
    static {
        try {
            //获取state变量在类TestUnSafe中的偏移值(2.2.4)
            stateOffset = unsafe.objectFieldOffset(TestUnSafe.class.
                    getDeclaredField("state"));
        } catch (Exception ex) {
            System.out.println(ex.getLocalizedMessage());
            throw new Error(ex);
        }
    }
    public static void main(String[] args) {
        //创建实例,并且设置state值为1(2.2.5)
        TestUnSafe test = new TestUnSafe();
        //(2.2.6)
        Boolean sucess = unsafe.compareAndSwapInt(test, stateOffset, 0, 1);
        System.out.println(sucess);
    }
}

在如上代码中,代码(2.2.1)获取了 Unsafe 的一个实例,代码(2.2.3)创建了一个变量 state 并初始化为 0。

代码(2.2.4)使用 unsafe.objectFieldOffset 获取 TestUnSafe 类里面的 state 变量,

在 TestUnSafe 对象里面的内存偏移量地址并将其保存到 stateOffset 变量中。

代码(2.2.6)调用创建的 unsafe 实例的 compareAndSwapInt 方法,设置 test 对象的 state 变量的值。

具体意思是,如果 test 对象中内存偏移量为 stateOffset 的 state 变量的值为 0,则更新该值为 1。

运行上面的代码,我们期望输出 true,然而执行后会输出如下结果。

为找出原因,必然要查看 getUnsafe 的代码。

java 复制代码
private static final Unsafe theUnsafe = new Unsafe();
 public static Unsafe getUnsafe()
{
    //(2.2.7)
    Class localClass = Reflection.getCallerClass();
  //(2.2.8)
    if (! VM.isSystemDomainLoader(localClass.getClassLoader())) {
      throw new SecurityException("Unsafe");
    }
    return theUnsafe;
}
  //判断paramClassLoader是不是BootStrap类加载器(2.2.9)
 public static boolean isSystemDomainLoader(ClassLoader paramClassLoader)
  {
    return paramClassLoader == null;
  }

代码(2.2.7)获取调用 getUnsafe 这个方法的对象的 Class 对象,这里是 TestUnSafe.class。

代码(2.2.8)判断是不是 Bootstrap 类加载器加载的 localClass,在这里是看是不是 Bootstrap 加载器加载了 TestUnSafe.class。

很明显由于 TestUnSafe.class 是使用 AppClassLoader 加载的,所以这里直接抛出了异常。

思考一下,这里为何要有这个判断?我们知道 Unsafe 类是 rt.jar 包提供的,rt.jar 包里面的类是使用 Bootstrap 类加载器加载的,

而我们的启动 main 函数所在的类是使用 AppClassLoader 加载的,所以在 main 函数里面加载 Unsafe 类时,根据委托机制,会委托给Bootstrap 去加载 Unsafe 类。

如果没有代码(2.2.8)的限制,那么我们的应用程序就可以随意使用 Unsafe 做事情了,而 Unsafe 类可以直接操作内存,这是不安全的,所以JDK 开发组特意做了这个限制,不让开发人员在正规渠道使用 Unsafe 类,而是在 rt.jar 包里面的核心类中使用 Unsafe 功能。

如果开发人员真的想要实例化 Unsafe 类,那该如何做?

方法有多种,既然从正规渠道访问不了,那么就玩点黑科技,使用万能的反射来获取 Unsafe 实例方法。

java 复制代码
package com.yangjunbo.juc.two;

import sun.misc.Unsafe;

import java.lang.reflect.Field;

/**
 * ClassName: TestUnSafe2
 * Package: com.yangjunbo.juc.two
 * Description:
 *
 * @Author 杨钧博
 * @Create 2026/10/20 15:41
 * @Version 1.0
 */
public class TestUnSafe2 {
    static final Unsafe unsafe;
    static final long stateOffset;
    private volatile long state = 0;
    static {
        try {
            //使用反射获取Unsafe的成员变量theUnsafe
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            // 设置为可存取
            field.setAccessible(true);
            // 获取该变量的值
            unsafe = (Unsafe) field.get(null);
            //获取state在TestUnSafe中的偏移量
            stateOffset = unsafe.objectFieldOffset(TestUnSafe.class.
                    getDeclaredField("state"));
        } catch (Exception ex) {
            System.out.println(ex.getLocalizedMessage());
            throw new Error(ex);
        }
    }
    public static void main(String[] args) {
        TestUnSafe2 test = new TestUnSafe2();
        Boolean sucess = unsafe.compareAndSwapInt(test, stateOffset, 0, 1);
        System.out.println(sucess);
    }
}

在如上代码中,通过代码(2.2.10)​、代码(2.2.11)和代码(2.2.12)反射获取 unsafe 的实例,运行后输出结果如下。

相关推荐
墨子夕2 小时前
设计模式——Java × Python 双语言精讲 · 23 个经典模式 + 现代演进(第一篇)
java·python·设计模式
程序 代码狂人2 小时前
经典 SQL 面试题:连续登录问题的两种经典解法
java·开发语言·算法
YB13752 小时前
Jetpack Compose 动画updateTransition
android
weixin_BYSJ19872 小时前
【计算机毕设】房屋租赁系统02647
java·vue.js·spring boot·python·django·课程设计
用户2018792831672 小时前
给主厨装一台打卡机:一文搞懂 Android APM 性能监控原理
android
VX_bysjlw9852 小时前
基于协同过滤的美食推荐系统的设计与实现32221-计算机毕设原创(免费领源码+带部署教程)
java·spring boot·redis·mysql·tomcat·maven·mybatis
Misnice2 小时前
Python 的数据类型
java·windows·python