$ java -version
java version “13.0.2” 2020-01-14
Java(TM) SE Runtime Environment (build 13.0.2+8)
Java HotSpot(TM) 64-Bit Server VM (build 13.0.2+8, mixed mode, sharing)
Java 中的 synchronized 关键字浅析
题外话
我们常说, 要避免多线程的上下文切换。
这里我想问两个问题:
- 什么是上下文切换?
- 为什么要避免上下文切换?
在回答上面的问题之前, 我们先大致了解下 CPU 的内部结构(只列出关键部分, 图示为一个包含两个核心的 CPU)
如图所示, 一个 CPU 包含多个核心, 每个核心除了包含逻辑计算单元(ALU)、寄存器、程序计数器等外, 还会囊括 L1、L2 两级缓存。
L3 缓存为 CPU 级别的缓存, 主存则是所有 CPU 共享。
CPU 执行速度非常快,快到主存的读写速度跟不上,于是出现了 L1、L2、L3 三级缓存的设置。至于为什么是 3 级缓存,以及各级缓存是不是越大越好,作者作为一只菜鸡,就不得而知了。
好了, 在了解了 CPU 的基本架构后, 我们来说说第一个问题:
- 什么是上下文切换?
在我之前的文章有提到:
CPU 通过时间片分配算法来循环执行任务, 一个时间片一般为几十毫秒,而时间片是用来给各个线程运行指令的时间。
当线程 A 消耗完时间片后, CPU 需要保存当前线程的状态,这样,当时间片再次分配给线程 A 时,可以知道它之前的执行状态。
这里,从保存到在加载的过程,就是一次上下文切换。
参考上图, 有一个线程 t1 在 core1 执行, 时间片结束后, 轮到 t2 执行,在 t2 执行前, 需要先把 t1 的上下文保存起来,等到 t1 再次轮到时,先把之前保存起来的上下文恢复,再开始执行 t1。 这样的一个过程,就是上下文切换。
知道上下文切换的含义后, 第二个问题自然而然就明白了,避免上下文切换,就可以避免保存和重新加载上下文,避免对 CPU 的浪费。
用户态和内核态
计算机硬件资源是有限的,如果所有的应用程序都能任意申请和使用有限的硬件资源,那 server 很快就会被玩坏。
于是 Linux 操作系统针对不同的操作,赋予不同的权限,而不同的两级权限,对应的就是我们的用户态 和 内核态。
用户态如果想申请硬件资源如内存分配,需要通过系统调用,由内核态来分配。
完成一次系统调用,需要先将当前的用户态信息保存下来, 然后切换到内核态执行,得到结果后,再切回用户态并恢复现场
轻量级锁和重量级锁
我们说这把锁是轻量级锁,意思是这把锁在用户态就能完成,不会进行系统调用,即不会有用户态、内核态的切换。 重量级锁则会发生系统调用。
java 中的轻量级锁,即 CAS,compareAndSwap,即 自旋锁。 CAS 是在用户态调用。
我们查看 java 的代码,可以看到这样一个本地方法:
// jdk.internal.misc.Unsafe#getAndAddInt -- 1
@HotSpotIntrinsicCandidate
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
// jdk.internal.misc.Unsafe#compareAndSetInt -- 2
@HotSpotIntrinsicCandidate
public final native boolean compareAndSetInt(Object o, long offset,
int expected,
int x);
2 处是一个 native 方法,我们需要知道的是,该方法能向我们保证是一个原子操作。(通过 hotspot 的源码可以知道, 是通过 lock cmpxchg 指令实现原子操作的)。
1 处则是通过 do... while...
自旋的方式,不断尝试设置新的值,直到成功。
所以我们说, CAS 是自旋锁
对于重量级锁, 用户态程序需要首先向操作系统申请锁, 简言之,就是所有的控制权都交给了操作系统,由操作系统来负责线程间的调度和线程的状态变更。而这样会出现频繁地对线程运行状态的切换,线程的挂起和唤醒,从而消耗大量的系统资。