提交 1d1dbced 编写于 作者: 沉默王二's avatar 沉默王二 💬

同步

上级 62fe44cf
...@@ -24,9 +24,7 @@ head: ...@@ -24,9 +24,7 @@ head:
回到宿舍翻了翻《[二哥的 Java 进阶之路并发编程篇](https://javabetter.cn/thread/jmm.html)》,小二才恍然大悟,原来自己弄错了概念,面试官是想考察 JMM,但是小二一听到`Java内存`这几个关键字就开始背Java 运行时内存区域的八股文了,害,Java 内存模型(JMM)和 Java 运行时内存区域的区别可大着呢。 回到宿舍翻了翻《[二哥的 Java 进阶之路并发编程篇](https://javabetter.cn/thread/jmm.html)》,小二才恍然大悟,原来自己弄错了概念,面试官是想考察 JMM,但是小二一听到`Java内存`这几个关键字就开始背Java 运行时内存区域的八股文了,害,Java 内存模型(JMM)和 Java 运行时内存区域的区别可大着呢。
Java 内存模型(Java Memory Model,JMM)定义了 Java 程序中的变量、线程如何和主存以及工作内存进行交互的规则。它主要涉及到多线程环境下的共享变量可见性、指令重排等问题,是理解并发编程中的关键概念。我们来详细地看一下。 Java 内存模型(Java Memory Model,JMM)定义了 Java 程序中的变量、线程如何和主存以及工作内存进行交互的规则。它主要涉及到多线程环境下的共享变量可见性、指令重排等问题,是理解并发编程中的关键概念。
## 线程如何通信和同步?
并发编程的线程之间存在两个问题: 并发编程的线程之间存在两个问题:
...@@ -111,6 +109,14 @@ Java 中的 [volatile 关键字](https://javabetter.cn/thread/volatile.html)可 ...@@ -111,6 +109,14 @@ Java 中的 [volatile 关键字](https://javabetter.cn/thread/volatile.html)可
总结一下: 总结一下:
Java 运行时内存区域描述的是在 JVM 运行时,如何将内存划分为不同的区域,并且每个区域的功能和工作机制。主要包括以下几个部分:
- 方法区:存储了每一个类的结构信息,如运行时常量池、字段和方法数据、构造方法和普通方法的字节码内容。
- 堆:几乎所有的对象实例以及数组都在这里分配内存。这是 Java 内存管理的主要区域。
- 栈:每一个线程有一个私有的栈,每一次方法调用都会创建一个新的栈帧,用于存储局部变量、操作数栈、动态链接、方法出口等信息。所有的栈帧都是在方法调用和方法执行完成之后创建和销毁的。
- 本地方法栈:与栈类似,不过本地方法栈为 JVM 使用到的 [native 方法](https://javabetter.cn/oo/native-method.html)服务。
- 程序计数器:每个线程都有一个独立的程序计数器,用于指示当前线程执行到了字节码的哪一行。
Java 内存模型 (JMM) 主要针对的是多线程环境下,如何在主内存与工作内存之间安全地执行操作。 Java 内存模型 (JMM) 主要针对的是多线程环境下,如何在主内存与工作内存之间安全地执行操作。
![](https://cdn.tobebetterjavaer.com/stutymore/jmm-20230823200720.png) ![](https://cdn.tobebetterjavaer.com/stutymore/jmm-20230823200720.png)
...@@ -121,19 +127,18 @@ Java 内存模型 (JMM) 主要针对的是多线程环境下,如何在主内 ...@@ -121,19 +127,18 @@ Java 内存模型 (JMM) 主要针对的是多线程环境下,如何在主内
- 原子性:一个或多个操作在整个过程中,不会被其他的线程或者操作所打断,这些操作是一个整体,要么都执行,要么都不执行。 - 原子性:一个或多个操作在整个过程中,不会被其他的线程或者操作所打断,这些操作是一个整体,要么都执行,要么都不执行。
- 有序性:程序执行的顺序按照代码的先后顺序执行的。 - 有序性:程序执行的顺序按照代码的先后顺序执行的。
Java 运行时内存区域描述的是在 JVM 运行时,如何将内存划分为不同的区域,并且每个区域的功能和工作机制。主要包括以下几个部分:
- 方法区:存储了每一个类的结构信息,如运行时常量池、字段和方法数据、构造方法和普通方法的字节码内容。 ## JMM 与重排序
- 堆:几乎所有的对象实例以及数组都在这里分配内存。这是 Java 内存管理的主要区域。
- 栈:每一个线程有一个私有的栈,每一次方法调用都会创建一个新的栈帧,用于存储局部变量、操作数栈、动态链接、方法出口等信息。所有的栈帧都是在方法调用和方法执行完成之后创建和销毁的。 前面提到了,JMM 定义了多线程之间如何互相交互的规则,主要目的是为了解决由于编译器优化、处理器优化和缓存系统等导致的可见性、原子性和有序性。
- 本地方法栈:与栈类似,不过本地方法栈为 JVM 使用到的 [native 方法](https://javabetter.cn/oo/native-method.html)服务。
- 程序计数器:每个线程都有一个独立的程序计数器,用于指示当前线程执行到了字节码的哪一行。
## 指令重排序 那我们接下来就来聊聊重排序以及它所带来的顺序问题。
计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排。 ### 为什么指令重排可以提高性能?
那可能有小伙伴就要问:为什么指令重排序可以提高性能? 大家都知道,计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排。
那可能有小伙伴就要问:**为什么指令重排序可以提高性能?**
简单地说,每一个指令都会包含多个步骤,每个步骤可能使用不同的硬件。因此,**流水线技术**产生了,它的原理是指令 1 还没有执行完,就可以开始执行指令 2,而不用等到指令 1 执行结束后再执行指令 2,这样就大大提高了效率。 简单地说,每一个指令都会包含多个步骤,每个步骤可能使用不同的硬件。因此,**流水线技术**产生了,它的原理是指令 1 还没有执行完,就可以开始执行指令 2,而不用等到指令 1 执行结束后再执行指令 2,这样就大大提高了效率。
...@@ -146,33 +151,27 @@ a = b + c; ...@@ -146,33 +151,27 @@ a = b + c;
d = e - f ; d = e - f ;
``` ```
先加载 b、c(**注意,有可能先加载 b,也有可能先加载 c**),但是在执行 `add(b,c)` 的时候,需要等待 b、c 装载结束才能继续执行,也就是增加了停顿,那么后面的指令也会有停顿,这就降低了计算机的执行效率。 先加载 b、c(**注意,有可能先加载 b,也有可能先加载 c**),但是在执行 `add(b,c)` 的时候,需要等待 b、c 装载结束才能继续执行,也就是需要增加停顿,那么后面的指令(加载 e 和 f)也会有停顿,这就降低了计算机的执行效率。
为了减少这个停顿,我们可以先加载 e 和 f,然后再去加载 `add(b,c)`,这样做对程序(串行)是没有影响的,但却减少了停顿。既然 `add(b,c)` 需要停顿,那还不如去做一些有意义的事情 为了减少停顿,我们可以在加载完 b 和 c 后把 e 和 f 也加载了,然后再去执行 `add(b,c)`,这样做对程序(串行)是没有影响的,但却减少了停顿
综上所述,**指令重排对于提高 CPU 性能十分必要。虽然由此带来了乱序的问题,但是这点牺牲是值得的。** 换句话说,既然 `add(b,c)` 需要停顿,那还不如去做一些有意义的事情(加载 e 和 f)。
指令重排一般分为以下三种: 综上所述,**指令重排对于提高 CPU 性能十分必要,但也带来了乱序的问题。**
- **编译器优化重排** ### 重排序有哪几种?
编译器在**不改变单线程程序语义**的前提下,可以重新安排语句的执行顺序。 指令重排一般分为以下三种:
- **指令并行重排**
现代处理器采用了指令级并行技术来将多条指令重叠执行。如果**不存在数据依赖性**(即后一个执行的语句无需依赖前面执行的语句的结果),处理器可以改变语句对应的机器指令的执行顺序。 - **编译器优化重排**,编译器在**不改变单线程程序语义**的前提下,重新安排语句的执行顺序。
- **内存系统重排** - **指令并行重排**,现代处理器采用了指令级并行技术来将多条指令重叠执行。如果**不存在数据依赖性**(即后一个执行的语句无需依赖前面执行的语句的结果),处理器可以改变语句对应的机器指令的执行顺序。
由于处理器使用缓存和读写缓存冲区,这使得加载(load)和存储(store)操作看上去可能是在乱序执行,因为三级缓存的存在,导致内存与缓存的数据同步存在时间差。 - **内存系统重排**由于处理器使用缓存和读写缓存冲区,这使得加载(load)和存储(store)操作看上去可能是在乱序执行,因为三级缓存的存在,导致内存与缓存的数据同步存在时间差。
**指令重排可以保证串行语义一致,但是没有义务保证多线程间的语义也一致**。所以在多线程下,指令重排序可能会导致一些问题。 **指令重排可以保证串行语义一致,但是没有义务保证多线程间的语义也一致**。所以在多线程下,指令重排序可能会导致一些问题。
## 顺序一致性模型与 JMM 的保证 ## JMM 与顺序一致性模型
顺序一致性模型是一个**理论参考模型**,内存模型在设计的时候都会以顺序一致性内存模型作为参考。
### 数据竞争与顺序一致性
当程序未正确同步的时候,就可能存在数据竞争。 当程序未正确同步的时候,就可能存在数据竞争。
...@@ -180,24 +179,17 @@ d = e - f ; ...@@ -180,24 +179,17 @@ d = e - f ;
如果程序中包含了数据竞争,那么运行的结果往往充满了**不确定性**,比如读发生在了写之前,可能就会读到错误的值;如果一个线程能够正确同步,那么就不存在数据竞争。 如果程序中包含了数据竞争,那么运行的结果往往充满了**不确定性**,比如读发生在了写之前,可能就会读到错误的值;如果一个线程能够正确同步,那么就不存在数据竞争。
Java 内存模型(JMM)对于正确同步多线程程序的内存一致性做了以下保证: Java 内存模型(JMM)对于正确同步多线程程序的内存一致性做了以下保证:**如果程序是正确同步的,程序的执行将具有顺序一致性**。即程序的执行结果和该程序在顺序一致性模型中执行的结果相同。
> **如果程序是正确同步的,程序的执行将具有顺序一致性**。即程序的执行结果和该程序在顺序一致性模型中执行的结果相同。
这里的同步包括了使用`volatile``final``synchronized`等关键字来实现**多线程下的同步**
如果程序员没有正确使用`volatile``final``synchronized`,那么即便是使用了同步(单线程下的同步),JMM 也不会有内存可见性的保证,可能会导致你的程序出错,并且具有不可重现性,很难排查。
所以如何正确使用`volatile``final``synchronized`,是我们 Java 程序员应该去了解的 这里的同步包括使用 [volatile](https://javabetter.cn/thread/volatile.html)[final](https://javabetter.cn/oo/final.html)[synchronized](https://javabetter.cn/thread/synchronized-1.html) 等关键字实现的同步
### 顺序一致性模型 如果我们开发者没有正确使用`volatile``final``synchronized` 等关键字,那么即便是使用了同步,JMM 也不会有内存可见性的保证,很可能会导致程序出错,并且不可重现,很难排查。
顺序一致性模型是一个**理想化的理论参考模型**,它为程序提供了极强的内存可见性保证。 ### 什么是顺序一致性模型?
顺序一致性模型有两大特性: 顺序一致性模型是一个**理想化的理论参考模型**,它为程序提供了极强的内存可见性保证。顺序一致性模型有两大特性:
- 一个线程中的所有操作必须按照程序的顺序(即 Java 代码的顺序)来执行。 - 一个线程中的所有操作必须按照程序的顺序(即 Java 代码的顺序)来执行。
- 不管程序是否同步,所有线程都只能看到一个单一的操作执行顺序。即在顺序一致性模型中,每个操作必须是**原子性的,且立刻对所有线程可见** - 不管程序是否同步,所有线程都只能看到一个单一的操作执行顺序。即在顺序一致性模型中,每个操作必须是**原子性的,且立刻对所有线程可见**
为了理解这两个特性,我们举个例子,假设有两个线程 A 和 B 并发执行,线程 A 有 3 个操作,他们在程序中的顺序是 A1->A2->A3,线程 B 也有 3 个操作,B1->B2->B3。 为了理解这两个特性,我们举个例子,假设有两个线程 A 和 B 并发执行,线程 A 有 3 个操作,他们在程序中的顺序是 A1->A2->A3,线程 B 也有 3 个操作,B1->B2->B3。
...@@ -208,6 +200,8 @@ Java 内存模型(JMM)对于正确同步多线程程序的内存一致性做 ...@@ -208,6 +200,8 @@ Java 内存模型(JMM)对于正确同步多线程程序的内存一致性做
操作的执行整体上有序,并且两个线程都只能看到这个执行顺序。 操作的执行整体上有序,并且两个线程都只能看到这个执行顺序。
### JMM 为什么不保证顺序一致性?
假设**没有使用同步**,那么在**顺序一致性模型**中的执行效果如下所示: 假设**没有使用同步**,那么在**顺序一致性模型**中的执行效果如下所示:
![没有正确同步图](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/thread/jmm-6357c025-a6e0-4c89-939d-040e549fac12.png) ![没有正确同步图](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/thread/jmm-6357c025-a6e0-4c89-939d-040e549fac12.png)
...@@ -216,9 +210,9 @@ Java 内存模型(JMM)对于正确同步多线程程序的内存一致性做 ...@@ -216,9 +210,9 @@ Java 内存模型(JMM)对于正确同步多线程程序的内存一致性做
**但是 JMM 没有这样的保证。** **但是 JMM 没有这样的保证。**
比如,在当前线程把写过的数据缓存在本地内存中,在没有刷新到主存之前,这个写操作仅对当前线程可见;从其他线程的角度来观察,这个写操作根本没有被当前线程所执行。只有当前线程把本地内存中写过的数据刷新到主存之后,这个写操作才对其他线程可见。在这种情况下,当前线程和其他线程看到的执行顺序是不一样的。 比如,在当前线程把写过的数据缓存在本地内存中,在没有刷新到主存之前,这个写操作仅对当前线程可见;从其他线程的角度来观察,这个写操作根本没有被当前线程所执行。
### JMM 中同步程序的顺序一致性 只有当前线程把本地内存中写过的数据刷新到主存之后,这个写操作才对其他线程可见。在这种情况下,当前线程和其他线程看到的执行顺序是不一样的。
在顺序一致性模型中,所有操作完全按照程序的顺序串行执行。但是 JMM 中,临界区内(同步块或同步方法中)的代码可以发生重排序(但不允许临界区内的代码“逃逸”到临界区之外,因为会破坏锁的内存语义)。 在顺序一致性模型中,所有操作完全按照程序的顺序串行执行。但是 JMM 中,临界区内(同步块或同步方法中)的代码可以发生重排序(但不允许临界区内的代码“逃逸”到临界区之外,因为会破坏锁的内存语义)。
...@@ -228,8 +222,6 @@ Java 内存模型(JMM)对于正确同步多线程程序的内存一致性做 ...@@ -228,8 +222,6 @@ Java 内存模型(JMM)对于正确同步多线程程序的内存一致性做
**由此可见,JMM 的具体实现方针是:在不改变(正确同步的)程序执行结果的前提下,尽量为编译期和处理器的优化打开方便之门** **由此可见,JMM 的具体实现方针是:在不改变(正确同步的)程序执行结果的前提下,尽量为编译期和处理器的优化打开方便之门**
### JMM 中未同步程序的顺序一致性
对于未同步的多线程,JMM 只提供**最小安全性**:线程读取到的值,要么是之前某个线程写入的值,要么是默认值,不会无中生有。 对于未同步的多线程,JMM 只提供**最小安全性**:线程读取到的值,要么是之前某个线程写入的值,要么是默认值,不会无中生有。
为了实现这个安全性,JVM 在堆上分配对象时,首先会对内存空间清零,然后才会在上面分配对象(这两个操作是同步的)。 为了实现这个安全性,JVM 在堆上分配对象时,首先会对内存空间清零,然后才会在上面分配对象(这两个操作是同步的)。
...@@ -242,7 +234,7 @@ Java 内存模型(JMM)对于正确同步多线程程序的内存一致性做 ...@@ -242,7 +234,7 @@ Java 内存模型(JMM)对于正确同步多线程程序的内存一致性做
2. 顺序一致性模型保证所有线程只能看到一致的操作执行顺序,而 JMM 不保证所有线程能看到一致的操作执行顺序。(因为 JMM 不保证所有操作立即可见) 2. 顺序一致性模型保证所有线程只能看到一致的操作执行顺序,而 JMM 不保证所有线程能看到一致的操作执行顺序。(因为 JMM 不保证所有操作立即可见)
3. 顺序一致性模型保证对所有的内存读写操作都具有原子性,而 JMM 不保证对 64 位的 long 型和 double 型变量的写操作具有原子性。 3. 顺序一致性模型保证对所有的内存读写操作都具有原子性,而 JMM 不保证对 64 位的 long 型和 double 型变量的写操作具有原子性。
## happens-before ## JMM 与 happens-before
一方面,我们开发者需要 JMM 提供一个强大的内存模型来编写代码;另一方面,编译器和处理器希望 JMM 对它们的束缚越少越好,这样它们就可以尽可能多的做优化来提高性能,希望的是一个弱的内存模型。 一方面,我们开发者需要 JMM 提供一个强大的内存模型来编写代码;另一方面,编译器和处理器希望 JMM 对它们的束缚越少越好,这样它们就可以尽可能多的做优化来提高性能,希望的是一个弱的内存模型。
...@@ -263,7 +255,7 @@ as-if-serial 语义保证单线程内重排序后的执行结果和程序代码 ...@@ -263,7 +255,7 @@ as-if-serial 语义保证单线程内重排序后的执行结果和程序代码
总之,**如果操作 A happens-before 操作 B,那么操作 A 在内存上所做的操作对操作 B 都是可见的,不管它们在不在一个线程。** 总之,**如果操作 A happens-before 操作 B,那么操作 A 在内存上所做的操作对操作 B 都是可见的,不管它们在不在一个线程。**
### 天然的 happens-before 关系 ### happens-before 关系有哪些?
在 Java 中,有以下天然的 happens-before 关系: 在 Java 中,有以下天然的 happens-before 关系:
......
...@@ -26,7 +26,7 @@ synchronized 关键字最主要有以下 3 种应用方式: ...@@ -26,7 +26,7 @@ synchronized 关键字最主要有以下 3 种应用方式:
- 同步静态方法,为当前类加锁(锁的是 [Class 对象](https://javabetter.cn/basic-extra-meal/fanshe.html)),进入同步代码前要获得当前类的锁; - 同步静态方法,为当前类加锁(锁的是 [Class 对象](https://javabetter.cn/basic-extra-meal/fanshe.html)),进入同步代码前要获得当前类的锁;
- 同步代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。 - 同步代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
## 同步方法 ## synchronized同步方法
通过在方法声明中加入 synchronized 关键字,可以保证在任意时刻,只有一个线程能执行该方法。 通过在方法声明中加入 synchronized 关键字,可以保证在任意时刻,只有一个线程能执行该方法。
...@@ -122,7 +122,7 @@ public class AccountingSyncBad implements Runnable { ...@@ -122,7 +122,7 @@ public class AccountingSyncBad implements Runnable {
参考:[对象和类](https://javabetter.cn/oo/object-class.html) 参考:[对象和类](https://javabetter.cn/oo/object-class.html)
## 同步静态方法 ## synchronized同步静态方法
当 synchronized 同步[静态方法](https://javabetter.cn/oo/static.html)时,锁的是当前类的 Class 对象,不属于某个对象。当前类的 Class 对象锁被获取,不影响实例对象锁的获取,两者互不影响,本质上是 this 和 Class 的不同。 当 synchronized 同步[静态方法](https://javabetter.cn/oo/static.html)时,锁的是当前类的 Class 对象,不属于某个对象。当前类的 Class 对象锁被获取,不影响实例对象锁的获取,两者互不影响,本质上是 this 和 Class 的不同。
...@@ -171,7 +171,7 @@ public class AccountingSyncClass implements Runnable { ...@@ -171,7 +171,7 @@ public class AccountingSyncClass implements Runnable {
注意代码中的 increase4Obj 方法是实例方法,其对象锁是当前实例对象(this),如果别的线程调用该方法,将不会产生互斥现象,毕竟锁的对象不同,这种情况下可能会发生[线程安全问题](https://javabetter.cn/thread/thread-bring-some-problem.html)(操作了共享静态变量 i)。 注意代码中的 increase4Obj 方法是实例方法,其对象锁是当前实例对象(this),如果别的线程调用该方法,将不会产生互斥现象,毕竟锁的对象不同,这种情况下可能会发生[线程安全问题](https://javabetter.cn/thread/thread-bring-some-problem.html)(操作了共享静态变量 i)。
## 同步代码块 ## synchronized同步代码块
某些情况下,我们编写的方法代码量比较多,存在一些比较耗时的操作,而需要同步的代码块只有一小部分,如果直接对整个方法进行同步,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹。 某些情况下,我们编写的方法代码量比较多,存在一些比较耗时的操作,而需要同步的代码块只有一小部分,如果直接对整个方法进行同步,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹。
...@@ -232,7 +232,7 @@ synchronized(AccountingSync.class){ ...@@ -232,7 +232,7 @@ synchronized(AccountingSync.class){
} }
``` ```
## 禁止指令重排分析 ## synchronized禁止指令重排
指令重排我们前面讲 [JMM](https://javabetter.cn/thread/jmm.html) 的时候讲过, 这里我们再结合 synchronized 关键字来讲一下。 指令重排我们前面讲 [JMM](https://javabetter.cn/thread/jmm.html) 的时候讲过, 这里我们再结合 synchronized 关键字来讲一下。
...@@ -267,7 +267,7 @@ class MonitorExample { ...@@ -267,7 +267,7 @@ class MonitorExample {
上图表示在线程 A 释放了锁之后,随后线程 B 获取同一个锁。在上图中,2 happens before 5。因此,线程 A 在释放锁之前所有可见的共享变量,在线程 B 获取同一个锁之后,将立刻变得对 B 线程可见。 上图表示在线程 A 释放了锁之后,随后线程 B 获取同一个锁。在上图中,2 happens before 5。因此,线程 A 在释放锁之前所有可见的共享变量,在线程 B 获取同一个锁之后,将立刻变得对 B 线程可见。
## 可重入锁 ## synchronized属于可重入锁
从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功。 从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功。
......
--- ---
title: synchronized到底锁的什么?偏向锁、轻量级锁、重量级锁到底是什么? title: synchronized到底锁的什么?偏向锁、轻量级锁、重量级锁到底是什么?
shortTitle: synchronized锁的是什么? shortTitle: 进击的synchronized
description: Java中的每一个对象都可以作为一个锁,这是synchronized实现同步的基础。当我们调用一个用synchronized关键字修饰的方法时,我们需要获取这个方法所在对象的锁。只有获取了这个锁,才可以执行这个方法。如果锁已经被其他线程获取,那么就会进入阻塞状态,直到锁被释放。 description: Java中的每一个对象都可以作为一个锁,这是synchronized实现同步的基础。当我们调用一个用synchronized关键字修饰的方法时,我们需要获取这个方法所在对象的锁。只有获取了这个锁,才可以执行这个方法。如果锁已经被其他线程获取,那么就会进入阻塞状态,直到锁被释放。
category: category:
- Java核心 - Java核心
...@@ -12,9 +12,9 @@ head: ...@@ -12,9 +12,9 @@ head:
content: Java,并发编程,多线程,Thread,synchronized,偏向锁,轻量级锁,重量级锁,锁 content: Java,并发编程,多线程,Thread,synchronized,偏向锁,轻量级锁,重量级锁,锁
--- ---
# 第十节:synchronized 锁的到底是什么? # 第十节:进击的synchronized
前面一节我们讲了 [synchronized 关键字的基本使用](https://javabetter.cn/thread/synchronized-1.html),它能用来同步方法和代码块,那 synchronized 到底锁的是什么呢? 前面一节我们讲了 [synchronized 关键字的基本使用](https://javabetter.cn/thread/synchronized-1.html),它能用来同步方法和代码块,那 synchronized 到底锁的是什么呢?随着 JDK 版本的升级,synchronized 又做出了哪些改变呢?“synchronized 性能很差”的谣言真的存在吗?
我想这是很多小伙伴感兴趣的。 我想这是很多小伙伴感兴趣的。
...@@ -30,7 +30,7 @@ Class 对象中包含了与类相关的很多信息,如类的名称、类的 ...@@ -30,7 +30,7 @@ Class 对象中包含了与类相关的很多信息,如类的名称、类的
所以我们常说的类锁,其实就是 Class 对象的锁。 所以我们常说的类锁,其实就是 Class 对象的锁。
## 重温 synchronized 的用法 ## 锁的基本用法
`synchronized` 翻译成中文就是“同步”的意思。 `synchronized` 翻译成中文就是“同步”的意思。
...@@ -90,9 +90,15 @@ public void blockLock() { ...@@ -90,9 +90,15 @@ public void blockLock() {
} }
``` ```
## 四种锁状态及锁降级 ## 锁的四种状态及锁降级
Java 6 为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁“。在 Java 6 以前,所有的锁都是”重量级“锁。所以在 Java 6 及其以后,一个对象其实有四种锁状态,它们级别由低到高依次是: 在 JDK 1.6 以前,所有的锁都是”重量级“锁,因为使用的是操作系统的互斥锁,当一个线程持有锁时,其他试图进入synchronized块的线程将被阻塞,直到锁被释放。涉及到了线程上下文切换和用户态与内核态的切换,因此效率较低。
这也是为什么很多开发者会认为 synchronized 性能很差的原因。
那为了减少获得锁和释放锁带来的性能消耗,JDK 1.6 引入了“偏向锁”和“轻量级锁” 的概念,对 synchronized 做了一次重大的升级,升级后的 synchronized 性能可以说上了一个新台阶。
在 JDK 1.6 及其以后,一个对象其实有四种锁状态,它们级别由低到高依次是:
1. 无锁状态 1. 无锁状态
2. 偏向锁状态 2. 偏向锁状态
...@@ -101,17 +107,17 @@ Java 6 为了减少获得锁和释放锁带来的性能消耗,引入了“偏 ...@@ -101,17 +107,17 @@ Java 6 为了减少获得锁和释放锁带来的性能消耗,引入了“偏
无锁就是没有对资源进行锁定,任何线程都可以尝试去修改它,很好理解。 无锁就是没有对资源进行锁定,任何线程都可以尝试去修改它,很好理解。
几种锁会随着竞争情况逐渐升级,锁的升级很容易发生,但是锁降级发生的条件就比较苛刻了,锁降级发生在 [Stop The World](https://javabetter.cn/jvm/gc.html)(Java 垃圾回收中的一个重要概念)期间,当 JVM 进入安全点的时候,会检查是否有闲置的锁,然后进行降级。 几种锁会随着竞争情况逐渐升级,锁的升级很容易发生,但是锁降级发生的条件就比较苛刻了,锁降级发生在 [Stop The World](https://javabetter.cn/jvm/gc.html)(Java 垃圾回收中的一个重要概念,JVM 篇会细讲)期间,当 JVM 进入安全点的时候,会检查是否有闲置的锁,然后进行降级。
关于锁降级有一点说明: 关于锁降级有一点需要说明:
不同于大部分文章说锁不能降级,实际上 HotSpot JVM 是支持锁降级的,[这篇帖子](https://openjdk.org/jeps/8183909)里有一个很关键的论述,帖子是 R 大给出的。 不同于大部分文章说锁不能降级,实际上 HotSpot JVM 是支持锁降级的,[这篇帖子](https://openjdk.org/jeps/8183909)里有一个很关键的论述,帖子是 R 大给出的。
> In its current implementation, monitor deflation is performed during every STW pause, while all Java threads are waiting at a safepoint. We have seen safepoint cleanup stalls up to 200ms on monitor-heavy-applications。 > In its current implementation, monitor deflation is performed during every STW pause, while all Java threads are waiting at a safepoint. We have seen safepoint cleanup stalls up to 200ms on monitor-heavy-applications。
大致的意思就是重量级锁降级发生于 STW(Stop The World)阶段,降级对象为仅仅能被 VMThread 访问而没有其他 JavaThread 访问的对象。 大致的意思就是重量级锁降级发生于 STW(Stop The World)阶段,降级对象为仅仅能被 VMThread 访问而没有其他 JavaThread 访问的对象。
各种锁的优缺点对比,下表来自《Java 并发编程的艺术》 各种锁的优缺点对比(来自《Java 并发编程的艺术》)
| 锁 | 优点 | 缺点 | 适用场景 | | 锁 | 优点 | 缺点 | 适用场景 |
| -------- | ------------------------------------------------------------------ | ------------------------------------------------ | ------------------------------------ | | -------- | ------------------------------------------------------------------ | ------------------------------------------------ | ------------------------------------ |
...@@ -119,15 +125,13 @@ Java 6 为了减少获得锁和释放锁带来的性能消耗,引入了“偏 ...@@ -119,15 +125,13 @@ Java 6 为了减少获得锁和释放锁带来的性能消耗,引入了“偏
| 轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度。 | 如果始终得不到锁竞争的线程使用自旋会消耗 CPU。 | 追求响应时间。同步块执行速度非常快。 | | 轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度。 | 如果始终得不到锁竞争的线程使用自旋会消耗 CPU。 | 追求响应时间。同步块执行速度非常快。 |
| 重量级锁 | 线程竞争不使用自旋,不会消耗 CPU。 | 线程阻塞,响应时间缓慢。 | 追求吞吐量。同步块执行时间较长。 | | 重量级锁 | 线程竞争不使用自旋,不会消耗 CPU。 | 线程阻塞,响应时间缓慢。 | 追求吞吐量。同步块执行时间较长。 |
## 锁升级 ## 对象的锁放在什么地方
下面分别介绍这几种锁以及它们之间是如何升级的。
前面我们提到,Java 的锁都是基于对象的。首先我们来看看一个对象的“锁”的信息是存放在什么地方的。 前面我们提到,Java 的锁都是基于对象的。
### Java 对象头 首先我们来看看一个对象的“锁”是存放在什么地方的。
每个 Java 对象都有对象头。如果是非数组类型,则用 2 个字宽来存储对象头,如果是数组,则会用 3 个字宽来存储对象头。在 32 位处理器中,一个字宽是 32 位;在 64 位虚拟机中,一个字宽是 64 位。对象头的内容如下表所示: 每个 Java 对象都有一个对象头。如果是非数组类型,则用 2 个字宽来存储对象头,如果是数组,则会用 3 个字宽来存储对象头。在 32 位处理器中,一个字宽是 32 位;在 64 位虚拟机中,一个字宽是 64 位。对象头的内容如下表所示:
| 长度 | 内容 | 说明 | | 长度 | 内容 | 说明 |
| -------- | ---------------------- | ------------------------------ | | -------- | ---------------------- | ------------------------------ |
...@@ -147,19 +151,21 @@ Java 6 为了减少获得锁和释放锁带来的性能消耗,引入了“偏 ...@@ -147,19 +151,21 @@ Java 6 为了减少获得锁和释放锁带来的性能消耗,引入了“偏
可以看到,当对象状态为偏向锁时,`Mark Word`存储的是偏向的线程 ID;当状态为轻量级锁时,`Mark Word`存储的是指向线程栈中`Lock Record`的指针;当状态为重量级锁时,`Mark Word`为指向堆中的 monitor(监视器)对象的指针。 可以看到,当对象状态为偏向锁时,`Mark Word`存储的是偏向的线程 ID;当状态为轻量级锁时,`Mark Word`存储的是指向线程栈中`Lock Record`的指针;当状态为重量级锁时,`Mark Word`为指向堆中的 monitor(监视器)对象的指针。
在 Java 中,监视器(monitor)是一种同步工具,用于保护对共享数据的访问,避免多线程并发访问导致数据不一致。在 Java 中,每个对象都有一个内置的监视器。 >在 Java 中,监视器(monitor)是一种同步工具,用于保护共享数据,避免多线程并发访问导致数据不一致。在 Java 中,每个对象都有一个内置的监视器。
监视器包括两个重要部分,一个是锁,一个是等待/通知机制,后者是通过 Object 类中的`wait()`, `notify()`, `notifyAll()`等方法实现的。 监视器包括两个重要部分,一个是锁,一个是等待/通知机制,后者是通过 Object 类中的`wait()`, `notify()`, `notifyAll()`等方法实现的(我们会在讲[Condition](https://javabetter.cn/thread/condition.html)[生产者-消费者模式](https://javabetter.cn/thread/shengchanzhe-xiaofeizhe.html))详细地讲
### 偏向锁 下面分别介绍这几种锁以及它们之间是如何升级的。
## 偏向锁
Hotspot 的作者经过以往的研究发现大多数情况下**锁不仅不存在多线程竞争,而且总是由同一线程多次获得**,于是引入了偏向锁。 Hotspot 的作者经过以往的研究发现大多数情况下**锁不仅不存在多线程竞争,而且总是由同一线程多次获得**,于是引入了偏向锁。
偏向锁会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。也就是说,**偏向锁在资源无竞争情况下消除了同步语句,连 [CAS](https://javabetter.cn/thread/cas.html) 操作都不做了,提高了程序的运行性能。** 偏向锁会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。也就是说,**偏向锁在资源无竞争情况下消除了同步语句**,连 [CAS](https://javabetter.cn/thread/cas.html)(后面会细讲,戳链接直达) 操作都不做了,着极大地提高了程序的运行性能。
大白话就是对锁设置个变量,如果发现为 true,代表资源无竞争,则无需再走各种加锁/解锁流程。如果为 false,代表存在其他线程竞争资源,那么就会走后面的流程。 大白话就是对锁设置个变量,如果发现为 true,代表资源无竞争,则无需再走各种加锁/解锁流程。如果为 false,代表存在其他线程竞争资源,那么就会走后面的流程。
#### **实现原理** ### 偏向锁的实现原理
一个线程在第一次进入同步块时,会在对象头和栈帧中的锁记录里存储锁偏向的线程 ID。当下次该线程进入这个同步块时,会去检查锁的 Mark Word 里面是不是放的自己的线程 ID。 一个线程在第一次进入同步块时,会在对象头和栈帧中的锁记录里存储锁偏向的线程 ID。当下次该线程进入这个同步块时,会去检查锁的 Mark Word 里面是不是放的自己的线程 ID。
...@@ -168,10 +174,9 @@ Hotspot 的作者经过以往的研究发现大多数情况下**锁不仅不存 ...@@ -168,10 +174,9 @@ Hotspot 的作者经过以往的研究发现大多数情况下**锁不仅不存
- 成功,表示之前的线程不存在了, Mark Word 里面的线程 ID 为新线程的 ID,锁不会升级,仍然为偏向锁; - 成功,表示之前的线程不存在了, Mark Word 里面的线程 ID 为新线程的 ID,锁不会升级,仍然为偏向锁;
- 失败,表示之前的线程仍然存在,那么暂停之前的线程,设置偏向锁标识为 0,并设置锁标志位为 00,升级为轻量级锁,会按照轻量级锁的方式进行竞争锁。 - 失败,表示之前的线程仍然存在,那么暂停之前的线程,设置偏向锁标识为 0,并设置锁标志位为 00,升级为轻量级锁,会按照轻量级锁的方式进行竞争锁。
> [CAS: Compare and Swap](https://javabetter.cn/thread/cas.html) [CAS: Compare and Swap](https://javabetter.cn/thread/cas.html) 会在后面细讲,可戳链接直达,这里简单提一嘴。
>
> 比较并设置。用于在硬件层面上提供原子性操作。在 Intel 处理器中,比较并交换通过指令 cmpxchg 实现。 CAS 是比较并设置的意思,用于在硬件层面上提供原子性操作。在 在某些处理器架构(如x86)中,比较并交换通过指令 CMPXCHG 实现((Compare and Exchange),一种原子指令),通过比较是否和给定的数值一致,如果一致则修改,不一致则不修改。
> 比较是否和给定的数值一致,如果一致则修改,不一致则不修改。
线程竞争偏向锁的过程如下: 线程竞争偏向锁的过程如下:
...@@ -179,9 +184,9 @@ Hotspot 的作者经过以往的研究发现大多数情况下**锁不仅不存 ...@@ -179,9 +184,9 @@ Hotspot 的作者经过以往的研究发现大多数情况下**锁不仅不存
图中涉及到了 lock record 指针指向当前堆栈中的最近一个 lock record,是轻量级锁按照先来先服务的模式进行了轻量级锁的加锁。 图中涉及到了 lock record 指针指向当前堆栈中的最近一个 lock record,是轻量级锁按照先来先服务的模式进行了轻量级锁的加锁。
#### 撤销偏向锁 ### 撤销偏向锁
偏向锁使用了一种**等到竞争出现才释放锁的机制**,所以当其他线程尝试竞争偏向锁时, 持有偏向锁的线程才会释放锁。 偏向锁使用了一种**等到竞争出现才释放锁的机制**,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。
偏向锁升级成轻量级锁时,会暂停拥有偏向锁的线程,重置偏向锁标识,这个过程看起来容易,实则开销还是很大的,大概的过程如下: 偏向锁升级成轻量级锁时,会暂停拥有偏向锁的线程,重置偏向锁标识,这个过程看起来容易,实则开销还是很大的,大概的过程如下:
...@@ -199,12 +204,10 @@ Hotspot 的作者经过以往的研究发现大多数情况下**锁不仅不存 ...@@ -199,12 +204,10 @@ Hotspot 的作者经过以往的研究发现大多数情况下**锁不仅不存
![](https://cdn.tobebetterjavaer.com/stutymore/synchronized-20230728112620.png) ![](https://cdn.tobebetterjavaer.com/stutymore/synchronized-20230728112620.png)
### 轻量级锁 ## 轻量级锁
多个线程在不同时段获取同一把锁,即不存在锁竞争的情况,也就没有线程阻塞。针对这种情况,JVM 采用轻量级锁来避免线程的阻塞与唤醒。 多个线程在不同时段获取同一把锁,即不存在锁竞争的情况,也就没有线程阻塞。针对这种情况,JVM 采用轻量级锁来避免线程的阻塞与唤醒。
#### 轻量级锁的加锁
JVM 会为每个线程在当前线程的栈帧中创建用于存储锁记录的空间,我们称为 Displaced Mark Word。如果一个线程获得锁的时候发现是轻量级锁,会把锁的 Mark Word 复制到自己的 Displaced Mark Word 里面。 JVM 会为每个线程在当前线程的栈帧中创建用于存储锁记录的空间,我们称为 Displaced Mark Word。如果一个线程获得锁的时候发现是轻量级锁,会把锁的 Mark Word 复制到自己的 Displaced Mark Word 里面。
然后线程尝试用 CAS 将锁的 Mark Word 替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示 Mark Word 已经被替换成了其他线程的锁记录,说明在与其它线程竞争锁,当前线程就尝试使用自旋来获取锁。 然后线程尝试用 CAS 将锁的 Mark Word 替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示 Mark Word 已经被替换成了其他线程的锁记录,说明在与其它线程竞争锁,当前线程就尝试使用自旋来获取锁。
...@@ -217,7 +220,7 @@ JVM 会为每个线程在当前线程的栈帧中创建用于存储锁记录的 ...@@ -217,7 +220,7 @@ JVM 会为每个线程在当前线程的栈帧中创建用于存储锁记录的
自旋也不是一直进行下去的,如果自旋到一定程度(和 JVM、操作系统相关),依然没有获取到锁,称为自旋失败,那么这个线程会阻塞。同时这个锁就会**升级成重量级锁** 自旋也不是一直进行下去的,如果自旋到一定程度(和 JVM、操作系统相关),依然没有获取到锁,称为自旋失败,那么这个线程会阻塞。同时这个锁就会**升级成重量级锁**
**轻量级锁的释放:** ### 轻量级锁的释放
在释放锁时,当前线程会使用 CAS 操作将 Displaced Mark Word 的内容复制回锁的 Mark Word 里面。如果没有发生竞争,那么这个复制的操作会成功。如果有其他线程因为自旋多次导致轻量级锁升级成了重量级锁,那么 CAS 操作会失败,此时会释放锁并唤醒被阻塞的线程。 在释放锁时,当前线程会使用 CAS 操作将 Displaced Mark Word 的内容复制回锁的 Mark Word 里面。如果没有发生竞争,那么这个复制的操作会成功。如果有其他线程因为自旋多次导致轻量级锁升级成了重量级锁,那么 CAS 操作会失败,此时会释放锁并唤醒被阻塞的线程。
...@@ -225,9 +228,9 @@ JVM 会为每个线程在当前线程的栈帧中创建用于存储锁记录的 ...@@ -225,9 +228,9 @@ JVM 会为每个线程在当前线程的栈帧中创建用于存储锁记录的
![](https://cdn.tobebetterjavaer.com/stutymore/synchronized-20230728114101.png) ![](https://cdn.tobebetterjavaer.com/stutymore/synchronized-20230728114101.png)
### 重量级锁 ## 重量级锁
重量级锁依赖于操作系统的互斥量(mutex) 实现的,而操作系统中线程间状态的转换需要相对较长的时间,所以重量级锁效率很低,但被阻塞的线程不会消耗 CPU。 重量级锁依赖于操作系统的互斥锁(mutex,用于保证任何给定时间内,只有一个线程可以执行某一段特定的代码段) 实现,而操作系统中线程间状态的转换需要相对较长的时间,所以重量级锁效率很低,但被阻塞的线程不会消耗 CPU。
前面说到,每一个对象都可以当做一个锁,当多个线程同时请求某个对象锁时,对象锁会设置几种状态用来区分请求的线程: 前面说到,每一个对象都可以当做一个锁,当多个线程同时请求某个对象锁时,对象锁会设置几种状态用来区分请求的线程:
...@@ -246,7 +249,7 @@ JVM 会为每个线程在当前线程的栈帧中创建用于存储锁记录的 ...@@ -246,7 +249,7 @@ JVM 会为每个线程在当前线程的栈帧中创建用于存储锁记录的
如果线程获得锁后调用`Object.wait`方法,则会将线程加入到 WaitSet 中,当被`Object.notify`唤醒后,会将线程从 WaitSet 移动到 Contention List 或 EntryList 中去。需要注意的是,当调用一个锁对象的`wait``notify`方法时,**如当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁** 如果线程获得锁后调用`Object.wait`方法,则会将线程加入到 WaitSet 中,当被`Object.notify`唤醒后,会将线程从 WaitSet 移动到 Contention List 或 EntryList 中去。需要注意的是,当调用一个锁对象的`wait``notify`方法时,**如当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁**
#### 锁的升级流程 ## 锁的升级流程
每一个线程在准备获取共享资源时: 每一个线程在准备获取共享资源时:
第一步,检查 MarkWord 里面是不是放的自己的 ThreadId ,如果是,表示当前线程是处于 “偏向锁” 。 第一步,检查 MarkWord 里面是不是放的自己的 ThreadId ,如果是,表示当前线程是处于 “偏向锁” 。
......
...@@ -12,29 +12,34 @@ head: ...@@ -12,29 +12,34 @@ head:
content: Java,并发编程,多线程,Thread,volatile content: Java,并发编程,多线程,Thread,volatile
--- ---
# 第八节:volatile关键字 # 第八节:volatile 关键字
“三妹啊,这节我们来学习 Java 中的 volatile 关键字吧,以及容易遇到的坑。”看着三妹好学的样子,我倍感欣慰。 “三妹啊,这节我们来学习 Java 中的 volatile 关键字吧,以及容易遇到的坑。”看着三妹好学的样子,我倍感欣慰。
“好呀,哥。”三妹愉快的答应了。 “好呀,哥。”三妹愉快的答应了。
>这是我们在《[二哥的 Java 进阶之路基础篇](https://javabetter.cn/overview/)》中常见的对话模式 > 这是我们在《[二哥的 Java 进阶之路基础篇](https://javabetter.cn/overview/)》中常见的对话模式,老读者应该对这种模式不陌生
## volatile 变量的特性 在讲[并发编程带来了哪些问题的时候](https://javabetter.cn/thread/thread-bring-some-problem.html),我们提到了可见性和原子性,那我现在可以直接告诉大家了:volatile 可以保证可见性,但不保证原子性:
volatile 可以保证可见性,但不保证原子性: - 当写一个 volatile 变量时,[JMM](https://javabetter.cn/thread/jmm.html) 会把该线程在本地内存中的变量强制刷新到主内存中去;
- 当写一个 volatile 变量时,[JMM](https://javabetter.cn/thread/jmm.html) 会把该线程本地内存中的变量强制刷新到主内存中去;
- 这个写操作会导致其他线程中的 volatile 变量缓存无效。 - 这个写操作会导致其他线程中的 volatile 变量缓存无效。
## volatile 会禁止指令重排 ## volatile 会禁止指令重排
我们回顾一下,重排序需要遵守的规则: 在讲 [JMM](https://javabetter.cn/thread/jmm.html) 的时候,我们提到了指令重排,相信大家都还有印象,我们来回顾一下重排序需要遵守的规则:
- 重排序不会对存在数据依赖关系的操作进行重排序。比如:`a=1;b=a;` 这个指令序列,因为第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。 - 重排序不会对存在数据依赖关系的操作进行重排序。比如:`a=1;b=a;` 这个指令序列,因为第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。
- 重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变。比如:`a=1;b=2;c=a+b` 这三个操作,第一步 (a=1) 和第二步 (b=2) 由于不存在数据依赖关系,所以可能会发生重排序,但是 c=a+b 这个操作是不会被重排序的,因为需要保证最终的结果一定是 c=a+b=3。 - 重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变。比如:`a=1;b=2;c=a+b` 这三个操作,第一步 (a=1) 和第二步 (b=2) 由于不存在数据依赖关系,所以可能会发生重排序,但是 c=a+b 这个操作是不会被重排序的,因为需要保证最终的结果一定是 c=a+b=3。
使用 volatile 关键字修饰共享变量可以禁止这种重排序。如果用 volatile 修饰共享变量,在编译时,会在指令序列中插入内存屏障来禁止重排序,规则如下: 使用 volatile 关键字修饰共享变量可以禁止这种重排序。怎么做到的呢?
当我们使用 volatile 关键字来修饰一个变量时,Java 内存模型会插入内存屏障(一个处理器指令,可以对 CPU 或编译器重排序做出约束)来确保以下两点:
- 写屏障(Write Barrier):当一个 volatile 变量被写入时,写屏障确保在该屏障之前的所有变量的写入操作都提交到主内存。
- 读屏障(Read Barrier):当读取一个 volatile 变量时,读屏障确保在该屏障之后的所有读操作都从主内存中读取。
换句话说:
- 当程序执行到 volatile 变量的读操作或者写操作时,在其前面操作的更改肯定已经全部进行,且结果对后面的操作可见;在其后面的操作肯定还没有进行; - 当程序执行到 volatile 变量的读操作或者写操作时,在其前面操作的更改肯定已经全部进行,且结果对后面的操作可见;在其后面的操作肯定还没有进行;
- 在进行指令优化时,不能将 volatile 变量的语句放在其后面执行,也不能把 volatile 变量后面的语句放到其前面执行。 - 在进行指令优化时,不能将 volatile 变量的语句放在其后面执行,也不能把 volatile 变量后面的语句放到其前面执行。
...@@ -60,7 +65,7 @@ class ReorderExample { ...@@ -60,7 +65,7 @@ class ReorderExample {
} }
``` ```
因为重排序影响,所以最终的输出可能是 0,具体分析请参考[上一篇](https://javabetter.cn/thread/jmm.html),如果引入 volatile,我们再看一下代码: 因为重排序影响,所以最终的输出可能是 0,重排序请参考[上一篇 JMM 的介绍](https://javabetter.cn/thread/jmm.html),如果引入 volatile,我们再看一下代码:
```java ```java
class ReorderExample { class ReorderExample {
...@@ -79,14 +84,12 @@ class ReorderExample { ...@@ -79,14 +84,12 @@ class ReorderExample {
} }
``` ```
个时候,volatile 禁止指令重排序也有一些规则,这个过程建立的 [happens before 关系](https://javabetter.cn/thread/jmm.html) 如下 时候,volatile 会禁止指令重排序,这个过程建立在 happens before 关系([上一篇介绍过了](https://javabetter.cn/thread/jmm.html))的基础上
1. 根据程序次序规则,1 happens before 2; 3 happens before 4。 1. 根据程序次序规则,1 happens before 2; 3 happens before 4。
2. 根据 volatile 规则,2 happens before 3。 2. 根据 volatile 规则,2 happens before 3。
3. 根据 happens before 的传递性规则,1 happens before 4。 3. 根据 happens before 的传递性规则,1 happens before 4。
>在 Java 中,"happens-before" 是一个用于描述两个或多个操作之间的偏序关系的概念,它来自于 Java 内存模型 ([上一篇](https://javabetter.cn/thread/jmm.html)有详细讲)。"happens-before" 关系可以确保在并发环境中的内存可见性,即一个线程中的写操作对于另一个线程中的读操作是可见的。
上述 happens before 关系的图形化表现形式如下: 上述 happens before 关系的图形化表现形式如下:
![](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/thread/volatile-f4de7989-672e-43d6-906b-feffe4fb0a9c.jpg) ![](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/thread/volatile-f4de7989-672e-43d6-906b-feffe4fb0a9c.jpg)
...@@ -134,13 +137,13 @@ inc output:8182 ...@@ -134,13 +137,13 @@ inc output:8182
“为什么呀?二哥?” 看到这个结果,三妹疑惑地问。 “为什么呀?二哥?” 看到这个结果,三妹疑惑地问。
“因为 inc++不是一个[原子性操作](https://javabetter.cn/thread/thread-bring-some-problem.html)(前面讲过),由读取、加、赋值 3 步组成,所以结果并不能达到 10000。”我耐心地回答。 “因为 inc++不是一个原子性操作([前面讲过](https://javabetter.cn/thread/thread-bring-some-problem.html)),由读取、加、赋值 3 步组成,所以结果并不能达到 10000。”我耐心地回答。
“哦,你这样说我就理解了。”三妹点点头。 “哦,你这样说我就理解了。”三妹点点头。
怎么解决呢? 怎么解决呢?
01、采用 [synchronized](https://javabetter.cn/thread/synchronized-1.html),把 `inc++` 拎出来单独加 synchronized 关键字: 01、采用 [synchronized](https://javabetter.cn/thread/synchronized-1.html)(下一篇会讲,戳链接直达),把 `inc++` 拎出来单独加 synchronized 关键字:
```java ```java
public class volatileTest1 { public class volatileTest1 {
...@@ -165,7 +168,7 @@ public class volatileTest1 { ...@@ -165,7 +168,7 @@ public class volatileTest1 {
} }
``` ```
02、采用 [Lock](https://javabetter.cn/thread/suo.html),通过重入锁 [ReentrantLock](https://javabetter.cn/thread/reentrantLock.html)`inc++` 加锁: 02、采用 [Lock](https://javabetter.cn/thread/suo.html),通过重入锁 [ReentrantLock](https://javabetter.cn/thread/reentrantLock.html)`inc++` 加锁(后面都会细讲,戳链接直达)
```java ```java
public class volatileTest2 { public class volatileTest2 {
...@@ -193,7 +196,7 @@ public class volatileTest2 { ...@@ -193,7 +196,7 @@ public class volatileTest2 {
} }
``` ```
03、采用原子类 [AtomicInteger](https://javabetter.cn/thread/atomic.html) 来实现: 03、采用原子类 [AtomicInteger](https://javabetter.cn/thread/atomic.html)(后面也会细讲,戳链接直达)来实现:
```java ```java
public class volatileTest3 { public class volatileTest3 {
...@@ -226,7 +229,7 @@ add lock, inc output:1000 ...@@ -226,7 +229,7 @@ add lock, inc output:1000
add AtomicInteger, inc output:1000 add AtomicInteger, inc output:1000
``` ```
## 单例模式的双重锁 ## volatile 实现单例模式的双重锁
这是一个使用"双重检查锁定"(double-checked locking)实现的单例模式(Singleton Pattern)的例子。 这是一个使用"双重检查锁定"(double-checked locking)实现的单例模式(Singleton Pattern)的例子。
...@@ -268,7 +271,7 @@ public class penguin { ...@@ -268,7 +271,7 @@ public class penguin {
- 初始化对象。 - 初始化对象。
- 将 m_penguin 指向分配的内存空间。 - 将 m_penguin 指向分配的内存空间。
如果不使用 volatile 关键字,JVM 可能会对这三个子步骤进行指令重排序,如果步骤2和步骤3被重排序,那么线程A可能在对象还没有被初始化完成时,线程B已经开始使用这个对象,从而导致问题。而使用 volatile 关键字可以防止这种指令重排序。 如果不使用 volatile 关键字,JVM 可能会对这三个子步骤进行指令重排序,如果步骤 2 和步骤 3 被重排序,那么线程 A 可能在对象还没有被初始化完成时,线程 B 已经开始使用这个对象,从而导致问题。而使用 volatile 关键字可以防止这种指令重排序。
伪代码代码如下: 伪代码代码如下:
...@@ -296,13 +299,12 @@ volatile 可以保证线程可见性且提供了一定的有序性,但是无 ...@@ -296,13 +299,12 @@ volatile 可以保证线程可见性且提供了一定的有序性,但是无
最后,我们学习了 volatile 不适用的场景,以及解决的方法,并解释了双重检查锁定实现的单例模式为何需要使用 volatile。 最后,我们学习了 volatile 不适用的场景,以及解决的方法,并解释了双重检查锁定实现的单例模式为何需要使用 volatile。
>编辑:沉默王二,编辑前的内容主要来自于二哥的[技术派](https://paicoding.com/)团队成员楼仔,原文链接戳:[volatile](https://paicoding.com/column/4/2)。 > 编辑:沉默王二,编辑前的内容主要来自于二哥的[技术派](https://paicoding.com/)团队成员楼仔,原文链接戳:[volatile](https://paicoding.com/column/4/2)。
----
GitHub 上标星 9000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括Java基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM等等,共计 32 万余字,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 9000+ 的 Java 教程](https://javabetter.cn/overview/) ---
GitHub 上标星 9000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括 Java 基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM 等等,共计 32 万余字,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 9000+ 的 Java 教程](https://javabetter.cn/overview/)
微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。 微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。
![](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongzhonghao.png) ![](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongzhonghao.png)
\ No newline at end of file
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册