Skip to content

Commit

Permalink
feat: JVM
Browse files Browse the repository at this point in the history
  • Loading branch information
lbwanga committed Sep 15, 2024
1 parent e1423e3 commit 6cfe64d
Show file tree
Hide file tree
Showing 73 changed files with 1,318 additions and 60 deletions.
103 changes: 72 additions & 31 deletions docs/JUC/JUC.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,8 @@ ObjectMonitor() {

锁一共有 4 种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。



#### 偏向锁

大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。
Expand All @@ -146,6 +148,12 @@ ObjectMonitor() {



一旦出现另外一个线程去尝试获取这个锁的情况,偏向模式就马上宣告结束。根据锁对象目前是否处于被锁定的状态决定是否撤销偏向,撤销后标志位恢复到未锁定或轻量级锁定的状态。

![](./JUC/偏向锁和轻量级锁状态转化.jpeg)



**偏向锁的撤销**

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。
Expand All @@ -168,7 +176,9 @@ ObjectMonitor() {

#### 轻量级锁

线程在执行同步块之前,JVM 会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的 Mark Word 复制到锁记录中,称为 `Displaced Mark Word`。然后线程尝试使用 CAS 将对象头中的 Mark Word 替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。如果自旋(默认10次)后还未等到锁,则说明目前竞争较重,需要膨胀为重量级锁。
线程在执行同步块之前,JVM 会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的 Mark Word 复制到锁记录中,称为 `Displaced Mark Word`。然后线程尝试使用 CAS 将对象头中的 Mark Word 替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,那就意味着至少存在一条线程与当前线程竞争获取该对象的锁。

虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是,说明当前线程已经拥有了这个对象的锁,那直接进入同步块继续执行就可以了,否则就说明这个锁对象已经被其他线程抢占了。如果出现两条以上的线程争用同一个锁的情况,那轻量级锁就不再有效,必须要膨胀为重量级锁。



Expand All @@ -178,6 +188,10 @@ ObjectMonitor() {



轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”这一经验法则。如果没有竞争,轻量级锁便通过CAS操作成功避免了使用互斥量的开销;但如果确实存在锁竞争,除了互斥量的本身开销外,还额外发生了CAS操作的开销。因此在有竞争的情况下,轻量级锁反而会比传统的重量级锁更慢。



#### 重量级锁

当出现较大竞争,锁膨胀为重量级锁时,对象头的`markword`指向堆中的`monitor`
Expand All @@ -198,29 +212,31 @@ ObjectMonitor() {

### JVM对Synchornized的优化

锁膨胀/锁升级、锁消除、锁粗话、自适应自旋锁
锁膨胀/锁升级、适应性自旋锁、锁消除、锁粗化、轻量级锁、偏向锁



**锁消除**
**适应性自旋锁**

锁消除是指对于被检测出不可能存在竞争的共享数据的锁进行消除,这是 JVM 即时编译器的优化
在线程尝试获取锁时,如果锁已经被其他线程占用,而当前线程的CPU资源没有耗尽,它会原地循环等待一段时间(自旋)而不进入阻塞状态。自旋等待期间,线程不会被挂起,从而避免了上下文切换的开销

锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当成私有数据对待,也就可以将它们的锁进行消除
适应性自旋意味着自旋的时间不再是固定的,而是会动态根据实际情况来改变



**锁粗化**
**锁消除**

对相同对象多次加锁,导致线程发生多次重入,频繁的加锁操作就会导致性能损耗,可以使用锁粗化方式优化
锁消除是指虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除

如果虚拟机探测到一串的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部
锁消除主要是通过逃逸分析来支持,如果堆上的共享数据不可能逃逸出去被其它线程访问到,那么就可以把它们当作栈上数据对待,也就可以将它们的锁进行消除



**自适应自旋锁**
**锁粗化**

对相同对象多次加锁,导致线程发生多次重入,频繁的加锁操作就会导致性能损耗,可以使用锁粗化方式优化。

线程空循环的次数并非固定的,而是会动态根据实际情况来改变自旋等待的次数
如果虚拟机探测到一串的操作都对同一个对象加锁,将会把加锁的范围扩展(粗化)到整个操作序列的外部



Expand Down Expand Up @@ -250,56 +266,79 @@ ObjectMonitor() {

## Java内存模型

Java内存模型(JMM)定义了Java程序中多线程访问主存和工作内存的规范,确保多线程并发访问时的可见性、有序性和原子性
Java内存模型的主要目的是定义程序中各种变量的访问规则,即关注在虚拟机中把变量值存储到内存和从内存中取出变量值这样的底层细节

线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是 JMM 的一个抽象概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
Java内存模型规定了所有的变量都存储在主内存中。每条线程还有自己的工作内存,线程的工作内存中保存了被该线程使用的变量的副本,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的数据。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

这里的主内存、工作内存与Java内存区域中的Java堆、栈、方法区等并不是同一个层次的对内存的划分,这两者基本上没有任何关系。如果两者一定要勉强对应起来,那么从变量、主内存、工作内存的定义来看,主内存主要对应于Java堆中的对象实例数据部分,而工作内存 则对应于虚拟机栈中的部分区域。从更基础的层次上说,主内存直接对应于物理硬件的内存,而为了获取更好的运行速度,虚拟机可能会让工作内存优先存储于寄存器和高速缓存中,因为程序运行时主要访问的是工作内存。

![](./JUC/JMM.png)



### 三大特性

**原子性**

一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性。

在 Java 中,可以借助`synchronized`、各种 `Lock` 以及各种原子类实现原子性。

**可见性**

指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

单线程程序不会出现内存可见性问题。但在多线程环境中可就不一定了,由于线程对共享变量的操作,都是拷贝到各自的工作内存运算的,运算完成后才刷回主内存中。另外指令重排以及编译器优化也可能导致可见性问题。

使用加锁或者`Volatile`关键字保证可见性。

**原子性**
`volatile、synchronized和final`关键字保证可见性。

一个或者多个操作在 CPU 执行的过程中不被中断的特性称为原子性。

在 Java 中,可以借助`synchronized`、各种 `Lock` 以及各种原子类实现原子性。

**有序性**

JMM 允许编译器和 CPU 优化指令顺序,但通过内存屏障机制和 `volatile` 等关键字可以保证线程间的执行顺序
如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程, 所有的操作都是无序的,指“指令重排序”现象和“工作内存与主内存同步延迟”现象

`happens-before` 规则来保证原子性、可见性以及有序性
Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性



### volatile

volatile 在多处理器开发中保证了共享变量的**可见性**。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。**主要作用是保证可见性和禁止指令重排优化。**

当一个变量被定义成volatile之后,它将具备两项特性:

1. 保证此变量对所有线程的可见性。而普通变量并不能做到这一点,普通变量的值在线程间传递时均需要通过主内存来完成。当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存;当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效,从主内存中读取共享变量。
2. 禁止指令重排序优化,普通的变量仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。

为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存后再进行操作,但操作完不知道何时会写到内存。如果对声明了 volatile 的变量进行写操作,JVM 就会向处理器发送一条 Lock 前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。

使用 volatile 修饰的共享变量,底层通过汇编 lock 前缀指令进行缓存锁定:

1. 将当前处理器(线程)缓存行的数据写回到系统内存。
2. 这个写回内存的操作会使在其他 CPU 里缓存了该内存地址的数据无效。

不能保证原子性:

```java
/**
* volatile变量自增运算测试
*/
public class VolatileTest {
public static volatile int race = 0;
public static void increase() {
race++;
}
}

当写一个 volatile 变量时,JMM 会把该线程对应的本地内存中的共享变量值刷新到主内存。
// 字节码
public static void increase();
Code:
Stack=2, Locals=0, Args_size=0
0: getstatic #13; //Field race:I
3: iconst_1
4: iadd
5: putstatic #13; //Field race:I
8: return
LineNumberTable:
line 14: 0
line 15: 8
```

当读一个 volatile 变量时,JMM 会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量
当getstatic指令把 race的值取到操作栈顶时,volatile关键字保证了race的值在此时是正确的,但是在执行iconst_1、iadd这些指令的时候,其他线程可能已经把race的值改变了,而操作栈顶的值就变成了过期的数据,所以 putstatic指令执行后就可能把较小的race值同步回主内存之中



Expand Down Expand Up @@ -364,8 +403,10 @@ happens-before规则:
2. 监视器锁规则:解锁于后续对这个锁的加锁可见。
3. volatile 变量规则:对一个 volatile 变量的写操作相对于后续对这个 volatile 变量的读操作可见。
4. 传递性规则:指如果 A Happens-Before B,且 B Happens-Before C,那么 A Happens-Before C。
5. 线程 start() 规则:指主线程 A 启动子线程 B 后,子线程 B 能够看到主线程A在启动子线程 B 前的操作。
6. 线程 join() 规则:指主线程 A 等待子线程 B 完成(主线程 A 通过调用子线程 B 的 join() 方法),当子线程 B 完成后(主线程 A 中 join() 方法返回),主线程能够看到子线程B的操作。
5. 线程启动规则:指线程 A 启动线程 B 后,线程 B 能够看到线程A在启动线程 B 前的操作。
6. 线程终止规则:指线程 A 等待线程 B 完成(线程 A 通过调用线程 B 的 join() 方法),当线程 B 完成后,线程A能够看到线程B的操作。
7. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
8. 对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize()方法的开始。



Expand Down Expand Up @@ -419,7 +460,7 @@ public class Singleton {
| Runnable(运行) | Java线程将操作系统的就绪和运行状态合并。调用了 t.start() 方法 |
| Blocked(阻塞) | 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入 Blocked 状态 |
| Waiting(等待) | 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入 Waiting 状态,进入这个状态后不能自动唤醒,必须等待另一个线程调用 notify 或者 notifyAll 方法才能唤醒。在这种状态下,线程将不会消耗CPU资源 |
| Timed Waiting (超时等待| 有几个方法有超时参数,调用将进入 Timed Waiting 状态,这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有 Thread.sleep 、Object.wait |
| Timed Waiting (限期等待| 有几个方法有超时参数,调用将进入 Timed Waiting 状态,这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有 Thread.sleep 、Object.wait |
| Teminated(终止) | run 方法正常退出而死亡,或者因为没有捕获的异常终止了 run 方法而死亡 |

![](./JUC/线程状态.jpeg)
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 6cfe64d

Please sign in to comment.