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

同步

上级 62fe44cf
......@@ -24,9 +24,7 @@ head:
回到宿舍翻了翻《[二哥的 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)可
总结一下:
Java 运行时内存区域描述的是在 JVM 运行时,如何将内存划分为不同的区域,并且每个区域的功能和工作机制。主要包括以下几个部分:
- 方法区:存储了每一个类的结构信息,如运行时常量池、字段和方法数据、构造方法和普通方法的字节码内容。
- 堆:几乎所有的对象实例以及数组都在这里分配内存。这是 Java 内存管理的主要区域。
- 栈:每一个线程有一个私有的栈,每一次方法调用都会创建一个新的栈帧,用于存储局部变量、操作数栈、动态链接、方法出口等信息。所有的栈帧都是在方法调用和方法执行完成之后创建和销毁的。
- 本地方法栈:与栈类似,不过本地方法栈为 JVM 使用到的 [native 方法](https://javabetter.cn/oo/native-method.html)服务。
- 程序计数器:每个线程都有一个独立的程序计数器,用于指示当前线程执行到了字节码的哪一行。
Java 内存模型 (JMM) 主要针对的是多线程环境下,如何在主内存与工作内存之间安全地执行操作。
![](https://cdn.tobebetterjavaer.com/stutymore/jmm-20230823200720.png)
......@@ -121,19 +127,18 @@ Java 内存模型 (JMM) 主要针对的是多线程环境下,如何在主内
- 原子性:一个或多个操作在整个过程中,不会被其他的线程或者操作所打断,这些操作是一个整体,要么都执行,要么都不执行。
- 有序性:程序执行的顺序按照代码的先后顺序执行的。
Java 运行时内存区域描述的是在 JVM 运行时,如何将内存划分为不同的区域,并且每个区域的功能和工作机制。主要包括以下几个部分:
- 方法区:存储了每一个类的结构信息,如运行时常量池、字段和方法数据、构造方法和普通方法的字节码内容。
- 堆:几乎所有的对象实例以及数组都在这里分配内存。这是 Java 内存管理的主要区域。
- 栈:每一个线程有一个私有的栈,每一次方法调用都会创建一个新的栈帧,用于存储局部变量、操作数栈、动态链接、方法出口等信息。所有的栈帧都是在方法调用和方法执行完成之后创建和销毁的。
- 本地方法栈:与栈类似,不过本地方法栈为 JVM 使用到的 [native 方法](https://javabetter.cn/oo/native-method.html)服务。
- 程序计数器:每个线程都有一个独立的程序计数器,用于指示当前线程执行到了字节码的哪一行。
## JMM 与重排序
前面提到了,JMM 定义了多线程之间如何互相交互的规则,主要目的是为了解决由于编译器优化、处理器优化和缓存系统等导致的可见性、原子性和有序性。
## 指令重排序
那我们接下来就来聊聊重排序以及它所带来的顺序问题。
计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排。
### 为什么指令重排可以提高性能?
那可能有小伙伴就要问:为什么指令重排序可以提高性能?
大家都知道,计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排。
那可能有小伙伴就要问:**为什么指令重排序可以提高性能?**
简单地说,每一个指令都会包含多个步骤,每个步骤可能使用不同的硬件。因此,**流水线技术**产生了,它的原理是指令 1 还没有执行完,就可以开始执行指令 2,而不用等到指令 1 执行结束后再执行指令 2,这样就大大提高了效率。
......@@ -146,33 +151,27 @@ a = b + c;
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 ;
如果程序中包含了数据竞争,那么运行的结果往往充满了**不确定性**,比如读发生在了写之前,可能就会读到错误的值;如果一个线程能够正确同步,那么就不存在数据竞争。
Java 内存模型(JMM)对于正确同步多线程程序的内存一致性做了以下保证:
> **如果程序是正确同步的,程序的执行将具有顺序一致性**。即程序的执行结果和该程序在顺序一致性模型中执行的结果相同。
这里的同步包括了使用`volatile``final``synchronized`等关键字来实现**多线程下的同步**
如果程序员没有正确使用`volatile``final``synchronized`,那么即便是使用了同步(单线程下的同步),JMM 也不会有内存可见性的保证,可能会导致你的程序出错,并且具有不可重现性,很难排查。
Java 内存模型(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 代码的顺序)来执行。
- 不管程序是否同步,所有线程都只能看到一个单一的操作执行顺序。即在顺序一致性模型中,每个操作必须是**原子性的,且立刻对所有线程可见**
为了理解这两个特性,我们举个例子,假设有两个线程 A 和 B 并发执行,线程 A 有 3 个操作,他们在程序中的顺序是 A1->A2->A3,线程 B 也有 3 个操作,B1->B2->B3。
......@@ -208,6 +200,8 @@ Java 内存模型(JMM)对于正确同步多线程程序的内存一致性做
操作的执行整体上有序,并且两个线程都只能看到这个执行顺序。
### JMM 为什么不保证顺序一致性?
假设**没有使用同步**,那么在**顺序一致性模型**中的执行效果如下所示:
![没有正确同步图](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/thread/jmm-6357c025-a6e0-4c89-939d-040e549fac12.png)
......@@ -216,9 +210,9 @@ Java 内存模型(JMM)对于正确同步多线程程序的内存一致性做
**但是 JMM 没有这样的保证。**
比如,在当前线程把写过的数据缓存在本地内存中,在没有刷新到主存之前,这个写操作仅对当前线程可见;从其他线程的角度来观察,这个写操作根本没有被当前线程所执行。只有当前线程把本地内存中写过的数据刷新到主存之后,这个写操作才对其他线程可见。在这种情况下,当前线程和其他线程看到的执行顺序是不一样的。
比如,在当前线程把写过的数据缓存在本地内存中,在没有刷新到主存之前,这个写操作仅对当前线程可见;从其他线程的角度来观察,这个写操作根本没有被当前线程所执行。
### JMM 中同步程序的顺序一致性
只有当前线程把本地内存中写过的数据刷新到主存之后,这个写操作才对其他线程可见。在这种情况下,当前线程和其他线程看到的执行顺序是不一样的。
在顺序一致性模型中,所有操作完全按照程序的顺序串行执行。但是 JMM 中,临界区内(同步块或同步方法中)的代码可以发生重排序(但不允许临界区内的代码“逃逸”到临界区之外,因为会破坏锁的内存语义)。
......@@ -228,8 +222,6 @@ Java 内存模型(JMM)对于正确同步多线程程序的内存一致性做
**由此可见,JMM 的具体实现方针是:在不改变(正确同步的)程序执行结果的前提下,尽量为编译期和处理器的优化打开方便之门**
### JMM 中未同步程序的顺序一致性
对于未同步的多线程,JMM 只提供**最小安全性**:线程读取到的值,要么是之前某个线程写入的值,要么是默认值,不会无中生有。
为了实现这个安全性,JVM 在堆上分配对象时,首先会对内存空间清零,然后才会在上面分配对象(这两个操作是同步的)。
......@@ -242,7 +234,7 @@ Java 内存模型(JMM)对于正确同步多线程程序的内存一致性做
2. 顺序一致性模型保证所有线程只能看到一致的操作执行顺序,而 JMM 不保证所有线程能看到一致的操作执行顺序。(因为 JMM 不保证所有操作立即可见)
3. 顺序一致性模型保证对所有的内存读写操作都具有原子性,而 JMM 不保证对 64 位的 long 型和 double 型变量的写操作具有原子性。
## happens-before
## JMM 与 happens-before
一方面,我们开发者需要 JMM 提供一个强大的内存模型来编写代码;另一方面,编译器和处理器希望 JMM 对它们的束缚越少越好,这样它们就可以尽可能多的做优化来提高性能,希望的是一个弱的内存模型。
......@@ -263,7 +255,7 @@ as-if-serial 语义保证单线程内重排序后的执行结果和程序代码
总之,**如果操作 A happens-before 操作 B,那么操作 A 在内存上所做的操作对操作 B 都是可见的,不管它们在不在一个线程。**
### 天然的 happens-before 关系
### happens-before 关系有哪些?
在 Java 中,有以下天然的 happens-before 关系:
......
......@@ -26,7 +26,7 @@ synchronized 关键字最主要有以下 3 种应用方式:
- 同步静态方法,为当前类加锁(锁的是 [Class 对象](https://javabetter.cn/basic-extra-meal/fanshe.html)),进入同步代码前要获得当前类的锁;
- 同步代码块,指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁。
## 同步方法
## synchronized同步方法
通过在方法声明中加入 synchronized 关键字,可以保证在任意时刻,只有一个线程能执行该方法。
......@@ -122,7 +122,7 @@ public class AccountingSyncBad implements Runnable {
参考:[对象和类](https://javabetter.cn/oo/object-class.html)
## 同步静态方法
## synchronized同步静态方法
当 synchronized 同步[静态方法](https://javabetter.cn/oo/static.html)时,锁的是当前类的 Class 对象,不属于某个对象。当前类的 Class 对象锁被获取,不影响实例对象锁的获取,两者互不影响,本质上是 this 和 Class 的不同。
......@@ -171,7 +171,7 @@ public class AccountingSyncClass implements Runnable {
注意代码中的 increase4Obj 方法是实例方法,其对象锁是当前实例对象(this),如果别的线程调用该方法,将不会产生互斥现象,毕竟锁的对象不同,这种情况下可能会发生[线程安全问题](https://javabetter.cn/thread/thread-bring-some-problem.html)(操作了共享静态变量 i)。
## 同步代码块
## synchronized同步代码块
某些情况下,我们编写的方法代码量比较多,存在一些比较耗时的操作,而需要同步的代码块只有一小部分,如果直接对整个方法进行同步,可能会得不偿失,此时我们可以使用同步代码块的方式对需要同步的代码进行包裹。
......@@ -232,7 +232,7 @@ synchronized(AccountingSync.class){
}
```
## 禁止指令重排分析
## synchronized禁止指令重排
指令重排我们前面讲 [JMM](https://javabetter.cn/thread/jmm.html) 的时候讲过, 这里我们再结合 synchronized 关键字来讲一下。
......@@ -267,7 +267,7 @@ class MonitorExample {
上图表示在线程 A 释放了锁之后,随后线程 B 获取同一个锁。在上图中,2 happens before 5。因此,线程 A 在释放锁之前所有可见的共享变量,在线程 B 获取同一个锁之后,将立刻变得对 B 线程可见。
## 可重入锁
## synchronized属于可重入锁
从互斥锁的设计上来说,当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁,请求将会成功。
......
---
title: synchronized到底锁的什么?偏向锁、轻量级锁、重量级锁到底是什么?
shortTitle: synchronized锁的是什么?
shortTitle: 进击的synchronized
description: Java中的每一个对象都可以作为一个锁,这是synchronized实现同步的基础。当我们调用一个用synchronized关键字修饰的方法时,我们需要获取这个方法所在对象的锁。只有获取了这个锁,才可以执行这个方法。如果锁已经被其他线程获取,那么就会进入阻塞状态,直到锁被释放。
category:
- Java核心
......@@ -12,9 +12,9 @@ head:
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 对象中包含了与类相关的很多信息,如类的名称、类的
所以我们常说的类锁,其实就是 Class 对象的锁。
## 重温 synchronized 的用法
## 锁的基本用法
`synchronized` 翻译成中文就是“同步”的意思。
......@@ -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. 无锁状态
2. 偏向锁状态
......@@ -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。
大致的意思就是重量级锁降级发生于 STW(Stop The World)阶段,降级对象为仅仅能被 VMThread 访问而没有其他 JavaThread 访问的对象。
各种锁的优缺点对比,下表来自《Java 并发编程的艺术》
各种锁的优缺点对比(来自《Java 并发编程的艺术》)
| 锁 | 优点 | 缺点 | 适用场景 |
| -------- | ------------------------------------------------------------------ | ------------------------------------------------ | ------------------------------------ |
......@@ -119,15 +125,13 @@ Java 6 为了减少获得锁和释放锁带来的性能消耗,引入了“偏
| 轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度。 | 如果始终得不到锁竞争的线程使用自旋会消耗 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 为了减少获得锁和释放锁带来的性能消耗,引入了“偏
可以看到,当对象状态为偏向锁时,`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 的作者经过以往的研究发现大多数情况下**锁不仅不存在多线程竞争,而且总是由同一线程多次获得**,于是引入了偏向锁。
偏向锁会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。也就是说,**偏向锁在资源无竞争情况下消除了同步语句,连 [CAS](https://javabetter.cn/thread/cas.html) 操作都不做了,提高了程序的运行性能。**
偏向锁会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。也就是说,**偏向锁在资源无竞争情况下消除了同步语句**,连 [CAS](https://javabetter.cn/thread/cas.html)(后面会细讲,戳链接直达) 操作都不做了,着极大地提高了程序的运行性能。
大白话就是对锁设置个变量,如果发现为 true,代表资源无竞争,则无需再走各种加锁/解锁流程。如果为 false,代表存在其他线程竞争资源,那么就会走后面的流程。
#### **实现原理**
### 偏向锁的实现原理
一个线程在第一次进入同步块时,会在对象头和栈帧中的锁记录里存储锁偏向的线程 ID。当下次该线程进入这个同步块时,会去检查锁的 Mark Word 里面是不是放的自己的线程 ID。
......@@ -168,10 +174,9 @@ Hotspot 的作者经过以往的研究发现大多数情况下**锁不仅不存
- 成功,表示之前的线程不存在了, Mark Word 里面的线程 ID 为新线程的 ID,锁不会升级,仍然为偏向锁;
- 失败,表示之前的线程仍然存在,那么暂停之前的线程,设置偏向锁标识为 0,并设置锁标志位为 00,升级为轻量级锁,会按照轻量级锁的方式进行竞争锁。
> [CAS: Compare and Swap](https://javabetter.cn/thread/cas.html)
>
> 比较并设置。用于在硬件层面上提供原子性操作。在 Intel 处理器中,比较并交换通过指令 cmpxchg 实现。
> 比较是否和给定的数值一致,如果一致则修改,不一致则不修改。
[CAS: Compare and Swap](https://javabetter.cn/thread/cas.html) 会在后面细讲,可戳链接直达,这里简单提一嘴。
CAS 是比较并设置的意思,用于在硬件层面上提供原子性操作。在 在某些处理器架构(如x86)中,比较并交换通过指令 CMPXCHG 实现((Compare and Exchange),一种原子指令),通过比较是否和给定的数值一致,如果一致则修改,不一致则不修改。
线程竞争偏向锁的过程如下:
......@@ -179,9 +184,9 @@ Hotspot 的作者经过以往的研究发现大多数情况下**锁不仅不存
图中涉及到了 lock record 指针指向当前堆栈中的最近一个 lock record,是轻量级锁按照先来先服务的模式进行了轻量级锁的加锁。
#### 撤销偏向锁
### 撤销偏向锁
偏向锁使用了一种**等到竞争出现才释放锁的机制**,所以当其他线程尝试竞争偏向锁时, 持有偏向锁的线程才会释放锁。
偏向锁使用了一种**等到竞争出现才释放锁的机制**,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。
偏向锁升级成轻量级锁时,会暂停拥有偏向锁的线程,重置偏向锁标识,这个过程看起来容易,实则开销还是很大的,大概的过程如下:
......@@ -199,12 +204,10 @@ Hotspot 的作者经过以往的研究发现大多数情况下**锁不仅不存
![](https://cdn.tobebetterjavaer.com/stutymore/synchronized-20230728112620.png)
### 轻量级锁
## 轻量级锁
多个线程在不同时段获取同一把锁,即不存在锁竞争的情况,也就没有线程阻塞。针对这种情况,JVM 采用轻量级锁来避免线程的阻塞与唤醒。
#### 轻量级锁的加锁
JVM 会为每个线程在当前线程的栈帧中创建用于存储锁记录的空间,我们称为 Displaced Mark Word。如果一个线程获得锁的时候发现是轻量级锁,会把锁的 Mark Word 复制到自己的 Displaced Mark Word 里面。
然后线程尝试用 CAS 将锁的 Mark Word 替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示 Mark Word 已经被替换成了其他线程的锁记录,说明在与其它线程竞争锁,当前线程就尝试使用自旋来获取锁。
......@@ -217,7 +220,7 @@ JVM 会为每个线程在当前线程的栈帧中创建用于存储锁记录的
自旋也不是一直进行下去的,如果自旋到一定程度(和 JVM、操作系统相关),依然没有获取到锁,称为自旋失败,那么这个线程会阻塞。同时这个锁就会**升级成重量级锁**
**轻量级锁的释放:**
### 轻量级锁的释放
在释放锁时,当前线程会使用 CAS 操作将 Displaced Mark Word 的内容复制回锁的 Mark Word 里面。如果没有发生竞争,那么这个复制的操作会成功。如果有其他线程因为自旋多次导致轻量级锁升级成了重量级锁,那么 CAS 操作会失败,此时会释放锁并唤醒被阻塞的线程。
......@@ -225,9 +228,9 @@ JVM 会为每个线程在当前线程的栈帧中创建用于存储锁记录的
![](https://cdn.tobebetterjavaer.com/stutymore/synchronized-20230728114101.png)
### 重量级锁
## 重量级锁
重量级锁依赖于操作系统的互斥量(mutex) 实现的,而操作系统中线程间状态的转换需要相对较长的时间,所以重量级锁效率很低,但被阻塞的线程不会消耗 CPU。
重量级锁依赖于操作系统的互斥锁(mutex,用于保证任何给定时间内,只有一个线程可以执行某一段特定的代码段) 实现,而操作系统中线程间状态的转换需要相对较长的时间,所以重量级锁效率很低,但被阻塞的线程不会消耗 CPU。
前面说到,每一个对象都可以当做一个锁,当多个线程同时请求某个对象锁时,对象锁会设置几种状态用来区分请求的线程:
......@@ -246,7 +249,7 @@ JVM 会为每个线程在当前线程的栈帧中创建用于存储锁记录的
如果线程获得锁后调用`Object.wait`方法,则会将线程加入到 WaitSet 中,当被`Object.notify`唤醒后,会将线程从 WaitSet 移动到 Contention List 或 EntryList 中去。需要注意的是,当调用一个锁对象的`wait``notify`方法时,**如当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁**
#### 锁的升级流程
## 锁的升级流程
每一个线程在准备获取共享资源时:
第一步,检查 MarkWord 里面是不是放的自己的 ThreadId ,如果是,表示当前线程是处于 “偏向锁” 。
......
......@@ -12,29 +12,34 @@ head:
content: Java,并发编程,多线程,Thread,volatile
---
# 第八节:volatile关键字
# 第八节: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 会禁止指令重排
我们回顾一下,重排序需要遵守的规则:
在讲 [JMM](https://javabetter.cn/thread/jmm.html) 的时候,我们提到了指令重排,相信大家都还有印象,我们来回顾一下重排序需要遵守的规则:
- 重排序不会对存在数据依赖关系的操作进行重排序。比如:`a=1;b=a;` 这个指令序列,因为第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。
- 重排序是为了优化性能,但是不管怎么重排序,单线程下程序的执行结果不能被改变。比如:`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 变量后面的语句放到其前面执行。
......@@ -60,7 +65,7 @@ class ReorderExample {
}
```
因为重排序影响,所以最终的输出可能是 0,具体分析请参考[上一篇](https://javabetter.cn/thread/jmm.html),如果引入 volatile,我们再看一下代码:
因为重排序影响,所以最终的输出可能是 0,重排序请参考[上一篇 JMM 的介绍](https://javabetter.cn/thread/jmm.html),如果引入 volatile,我们再看一下代码:
```java
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。
2. 根据 volatile 规则,2 happens before 3。
3. 根据 happens before 的传递性规则,1 happens before 4。
>在 Java 中,"happens-before" 是一个用于描述两个或多个操作之间的偏序关系的概念,它来自于 Java 内存模型 ([上一篇](https://javabetter.cn/thread/jmm.html)有详细讲)。"happens-before" 关系可以确保在并发环境中的内存可见性,即一个线程中的写操作对于另一个线程中的读操作是可见的。
上述 happens before 关系的图形化表现形式如下:
![](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/thread/volatile-f4de7989-672e-43d6-906b-feffe4fb0a9c.jpg)
......@@ -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
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
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
public class volatileTest3 {
......@@ -226,7 +229,7 @@ add lock, inc output:1000
add AtomicInteger, inc output:1000
```
## 单例模式的双重锁
## volatile 实现单例模式的双重锁
这是一个使用"双重检查锁定"(double-checked locking)实现的单例模式(Singleton Pattern)的例子。
......@@ -268,7 +271,7 @@ public class penguin {
- 初始化对象。
- 将 m_penguin 指向分配的内存空间。
如果不使用 volatile 关键字,JVM 可能会对这三个子步骤进行指令重排序,如果步骤2和步骤3被重排序,那么线程A可能在对象还没有被初始化完成时,线程B已经开始使用这个对象,从而导致问题。而使用 volatile 关键字可以防止这种指令重排序。
如果不使用 volatile 关键字,JVM 可能会对这三个子步骤进行指令重排序,如果步骤 2 和步骤 3 被重排序,那么线程 A 可能在对象还没有被初始化完成时,线程 B 已经开始使用这个对象,从而导致问题。而使用 volatile 关键字可以防止这种指令重排序。
伪代码代码如下:
......@@ -296,13 +299,12 @@ 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** 即可免费领取。
![](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongzhonghao.png)
\ No newline at end of file
![](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongzhonghao.png)
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册