synchronized.md 20.4 KB
Newer Older
沉默王二's avatar
沉默王二 已提交
1
---
沉默王二's avatar
同步  
沉默王二 已提交
2
title: synchronized到底锁的什么?偏向锁、轻量级锁、重量级锁到底是什么?
沉默王二's avatar
AQS  
沉默王二 已提交
3
shortTitle: synchronized的四种锁状态
沉默王二's avatar
同步  
沉默王二 已提交
4
description: Java中的每一个对象都可以作为一个锁,这是synchronized实现同步的基础。当我们调用一个用synchronized关键字修饰的方法时,我们需要获取这个方法所在对象的锁。只有获取了这个锁,才可以执行这个方法。如果锁已经被其他线程获取,那么就会进入阻塞状态,直到锁被释放。
沉默王二's avatar
沉默王二 已提交
5 6 7
category:
  - Java核心
tag:
沉默王二's avatar
沉默王二 已提交
8 9 10 11
  - Java并发编程
head:
  - - meta
    - name: keywords
沉默王二's avatar
同步  
沉默王二 已提交
12
      content: Java,并发编程,多线程,Thread,synchronized,偏向锁,轻量级锁,重量级锁,锁
沉默王二's avatar
沉默王二 已提交
13 14
---

沉默王二's avatar
AQS  
沉默王二 已提交
15
# 第十节:synchronized的四种锁状态
沉默王二's avatar
沉默王二 已提交
16

沉默王二's avatar
同步  
沉默王二 已提交
17
前面一节我们讲了 [synchronized 关键字的基本使用](https://javabetter.cn/thread/synchronized-1.html),它能用来同步方法和代码块,那 synchronized 到底锁的是什么呢?随着 JDK 版本的升级,synchronized 又做出了哪些改变呢?“synchronized 性能很差”的谣言真的存在吗?
沉默王二's avatar
沉默王二 已提交
18

沉默王二's avatar
同步  
沉默王二 已提交
19
我想这是很多小伙伴感兴趣的。
沉默王二's avatar
沉默王二 已提交
20

沉默王二's avatar
同步  
沉默王二 已提交
21
首先需要明确的一点是:**Java 多线程的锁都是基于对象的**,Java 中的每一个对象都可以作为一个锁。
沉默王二's avatar
沉默王二 已提交
22

沉默王二's avatar
同步  
沉默王二 已提交
23
还有一点需要注意的是,我们常听到的**类锁**其实也是对象锁,[上一节](https://javabetter.cn/thread/synchronized-1.html)我们也讲到了,应该有不少小伙伴注意到了。
沉默王二's avatar
沉默王二 已提交
24

沉默王二's avatar
沉默王二 已提交
25
这里再多说几句吧。Class 对象是一种特殊的 Java 对象,代表了程序中的类和接口。Java 中的每个类型(包括类、接口、数组以及基础类型)在 JVM 中都有一个唯一的 Class 对象与之对应。这个 Class 对象被创建的时机是在 JVM 加载类时,由 JVM 自动完成。
沉默王二's avatar
沉默王二 已提交
26

沉默王二's avatar
沉默王二 已提交
27
Class 对象中包含了与类相关的很多信息,如类的名称、类的父类、类实现的接口、类的构造方法、类的方法、类的字段等等。这些信息通常被称为元数据(metadata)。
沉默王二's avatar
沉默王二 已提交
28

沉默王二's avatar
沉默王二 已提交
29
可以通过 Class 对象来获取类的元数据,甚至动态地创建类的实例、调用类的方法、访问类的字段等。这就是[Java 的反射(Reflection)机制](https://javabetter.cn/basic-extra-meal/fanshe.html)
沉默王二's avatar
同步  
沉默王二 已提交
30 31 32

所以我们常说的类锁,其实就是 Class 对象的锁。

沉默王二's avatar
同步  
沉默王二 已提交
33
## 锁的基本用法
沉默王二's avatar
同步  
沉默王二 已提交
34 35 36 37

`synchronized` 翻译成中文就是“同步”的意思。

我们通常使用`synchronized`关键字来给一段代码或一个方法上锁,我们[上一节](https://javabetter.cn/thread/synchronized-1.html)已经讲过了,这里简单回顾一下,因为 synchronized 真的非常重要,面试常问,开发常用。它通常有以下三种形式:
沉默王二's avatar
沉默王二 已提交
38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58

```java
// 关键字在实例方法上,锁为当前实例
public synchronized void instanceLock() {
    // code
}

// 关键字在静态方法上,锁为当前Class对象
public static synchronized void classLock() {
    // code
}

// 关键字在代码块上,锁为括号里面的对象
public void blockLock() {
    Object o = new Object();
    synchronized (o) {
        // code
    }
}
```

沉默王二's avatar
同步  
沉默王二 已提交
59
这里介绍一下“临界区”的概念。所谓“临界区”,指的是某一块代码区域,它同一时刻只能由一个线程执行。在上面的例子中,如果`synchronized`关键字在方法上,那临界区就是整个方法内部。而如果是 synchronized 代码块,那临界区就指的是代码块内部的区域。
沉默王二's avatar
沉默王二 已提交
60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92

通过上面的例子我们可以看到,下面这两个写法其实是等价的作用:

```java
// 关键字在实例方法上,锁为当前实例
public synchronized void instanceLock() {
    // code
}

// 关键字在代码块上,锁为括号里面的对象
public void blockLock() {
    synchronized (this) {
        // code
    }
}
```

同理,下面这两个方法也应该是等价的:

```java
// 关键字在静态方法上,锁为当前Class对象
public static synchronized void classLock() {
    // code
}

// 关键字在代码块上,锁为括号里面的对象
public void blockLock() {
    synchronized (this.getClass()) {
        // code
    }
}
```

沉默王二's avatar
同步  
沉默王二 已提交
93
## 锁的四种状态及锁降级
沉默王二's avatar
沉默王二 已提交
94

沉默王二's avatar
同步  
沉默王二 已提交
95 96 97 98 99 100 101
在 JDK 1.6 以前,所有的锁都是”重量级“锁,因为使用的是操作系统的互斥锁,当一个线程持有锁时,其他试图进入synchronized块的线程将被阻塞,直到锁被释放。涉及到了线程上下文切换和用户态与内核态的切换,因此效率较低。

这也是为什么很多开发者会认为 synchronized 性能很差的原因。

那为了减少获得锁和释放锁带来的性能消耗,JDK 1.6 引入了“偏向锁”和“轻量级锁” 的概念,对 synchronized 做了一次重大的升级,升级后的 synchronized 性能可以说上了一个新台阶。

在 JDK 1.6 及其以后,一个对象其实有四种锁状态,它们级别由低到高依次是:
沉默王二's avatar
沉默王二 已提交
102 103 104 105 106 107

1. 无锁状态
2. 偏向锁状态
3. 轻量级锁状态
4. 重量级锁状态

沉默王二's avatar
同步  
沉默王二 已提交
108
无锁就是没有对资源进行锁定,任何线程都可以尝试去修改它,很好理解。
沉默王二's avatar
沉默王二 已提交
109

沉默王二's avatar
同步  
沉默王二 已提交
110
几种锁会随着竞争情况逐渐升级,锁的升级很容易发生,但是锁降级发生的条件就比较苛刻了,锁降级发生在 [Stop The World](https://javabetter.cn/jvm/gc.html)(Java 垃圾回收中的一个重要概念,JVM 篇会细讲)期间,当 JVM 进入安全点的时候,会检查是否有闲置的锁,然后进行降级。
沉默王二's avatar
沉默王二 已提交
111

沉默王二's avatar
同步  
沉默王二 已提交
112
关于锁降级有一点需要说明:
沉默王二's avatar
同步  
沉默王二 已提交
113

沉默王二's avatar
同步  
沉默王二 已提交
114
不同于大部分文章说的锁不能降级,实际上 HotSpot JVM 是支持锁降级的,[这篇帖子](https://openjdk.org/jeps/8183909)里有一个很关键的论述,帖子是 R 大给出的。
沉默王二's avatar
同步  
沉默王二 已提交
115

沉默王二's avatar
沉默王二 已提交
116
> 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。
沉默王二's avatar
沉默王二 已提交
117

沉默王二's avatar
沉默王二 已提交
118
大致的意思就是重量级锁降级发生于 STW(Stop The World)阶段,降级对象为仅仅能被 VMThread 访问而没有其他 JavaThread 访问的对象。
沉默王二's avatar
沉默王二 已提交
119

沉默王二's avatar
同步  
沉默王二 已提交
120
各种锁的优缺点对比(来自《Java 并发编程的艺术》):
沉默王二's avatar
沉默王二 已提交
121

沉默王二's avatar
沉默王二 已提交
122 123 124 125 126 127
| 锁       | 优点                                                               | 缺点                                             | 适用场景                             |
| -------- | ------------------------------------------------------------------ | ------------------------------------------------ | ------------------------------------ |
| 偏向锁   | 加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗。 | 适用于只有一个线程访问同步块场景。   |
| 轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度。                         | 如果始终得不到锁竞争的线程使用自旋会消耗 CPU。   | 追求响应时间。同步块执行速度非常快。 |
| 重量级锁 | 线程竞争不使用自旋,不会消耗 CPU。                                 | 线程阻塞,响应时间缓慢。                         | 追求吞吐量。同步块执行时间较长。     |

沉默王二's avatar
同步  
沉默王二 已提交
128
## 对象的锁放在什么地方
沉默王二's avatar
沉默王二 已提交
129

沉默王二's avatar
同步  
沉默王二 已提交
130
前面我们提到,Java 的锁都是基于对象的。
沉默王二's avatar
沉默王二 已提交
131

沉默王二's avatar
同步  
沉默王二 已提交
132
首先我们来看看一个对象的“锁”是存放在什么地方的。
沉默王二's avatar
沉默王二 已提交
133

沉默王二's avatar
同步  
沉默王二 已提交
134
每个 Java 对象都有一个对象头。如果是非数组类型,则用 2 个字宽来存储对象头,如果是数组,则会用 3 个字宽来存储对象头。在 32 位处理器中,一个字宽是 32 位;在 64 位虚拟机中,一个字宽是 64 位。对象头的内容如下表所示:
沉默王二's avatar
沉默王二 已提交
135

沉默王二's avatar
同步  
沉默王二 已提交
136 137 138 139 140 141 142
| 长度     | 内容                   | 说明                           |
| -------- | ---------------------- | ------------------------------ |
| 32/64bit | Mark Word              | 存储对象的 hashCode 或锁信息等 |
| 32/64bit | Class Metadata Address | 存储到对象类型数据的指针       |
| 32/64bit | Array length           | 数组的长度(如果是数组)       |

我们主要来看看 Mark Word 的格式:
沉默王二's avatar
沉默王二 已提交
143 144 145 146

| 锁状态   | 29 bit 或 61 bit             | 1 bit 是否是偏向锁?       | 2 bit 锁标志位 |
| -------- | ---------------------------- | -------------------------- | -------------- |
| 无锁     |                              | 0                          | 01             |
沉默王二's avatar
同步  
沉默王二 已提交
147
| 偏向锁   | 线程 ID                      | 1                          | 01             |
沉默王二's avatar
沉默王二 已提交
148 149
| 轻量级锁 | 指向栈中锁记录的指针         | 此时这一位不用于标识偏向锁 | 00             |
| 重量级锁 | 指向互斥量(重量级锁)的指针 | 此时这一位不用于标识偏向锁 | 10             |
沉默王二's avatar
同步  
沉默王二 已提交
150 151 152
| GC 标记  |                              | 此时这一位不用于标识偏向锁 | 11             |

可以看到,当对象状态为偏向锁时,`Mark Word`存储的是偏向的线程 ID;当状态为轻量级锁时,`Mark Word`存储的是指向线程栈中`Lock Record`的指针;当状态为重量级锁时,`Mark Word`为指向堆中的 monitor(监视器)对象的指针。
沉默王二's avatar
沉默王二 已提交
153

沉默王二's avatar
同步  
沉默王二 已提交
154
>在 Java 中,监视器(monitor)是一种同步工具,用于保护共享数据,避免多线程并发访问导致数据不一致。在 Java 中,每个对象都有一个内置的监视器。
沉默王二's avatar
同步  
沉默王二 已提交
155

沉默王二's avatar
同步  
沉默王二 已提交
156
监视器包括两个重要部分,一个是锁,一个是等待/通知机制,后者是通过 Object 类中的`wait()`, `notify()`, `notifyAll()`等方法实现的(我们会在讲[Condition](https://javabetter.cn/thread/condition.html)[生产者-消费者模式](https://javabetter.cn/thread/shengchanzhe-xiaofeizhe.html))详细地讲。
沉默王二's avatar
沉默王二 已提交
157

沉默王二's avatar
同步  
沉默王二 已提交
158 159 160
下面分别介绍这几种锁以及它们之间是如何升级的。

## 偏向锁
沉默王二's avatar
沉默王二 已提交
161

沉默王二's avatar
同步  
沉默王二 已提交
162
Hotspot 的作者经过以往的研究发现大多数情况下**锁不仅不存在多线程竞争,而且总是由同一线程多次获得**,于是引入了偏向锁。
沉默王二's avatar
沉默王二 已提交
163

沉默王二's avatar
同步  
沉默王二 已提交
164
偏向锁会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。也就是说,**偏向锁在资源无竞争情况下消除了同步语句**,连 [CAS](https://javabetter.cn/thread/cas.html)(后面会细讲,戳链接直达) 操作都不做了,着极大地提高了程序的运行性能。
沉默王二's avatar
沉默王二 已提交
165

沉默王二's avatar
同步  
沉默王二 已提交
166
大白话就是对锁设置个变量,如果发现为 true,代表资源无竞争,则无需再走各种加锁/解锁流程。如果为 false,代表存在其他线程竞争资源,那么就会走后面的流程。
沉默王二's avatar
沉默王二 已提交
167

沉默王二's avatar
同步  
沉默王二 已提交
168
### 偏向锁的实现原理
沉默王二's avatar
沉默王二 已提交
169

沉默王二's avatar
同步  
沉默王二 已提交
170
一个线程在第一次进入同步块时,会在对象头和栈帧中的锁记录里存储锁偏向的线程 ID。当下次该线程进入这个同步块时,会去检查锁的 Mark Word 里面是不是放的自己的线程 ID。
沉默王二's avatar
沉默王二 已提交
171

沉默王二's avatar
同步  
沉默王二 已提交
172
如果是,表明该线程已经获得了锁,以后该线程在进入和退出同步块时不需要花费 CAS 操作来加锁和解锁;如果不是,就代表有另一个线程来竞争这个偏向锁。这个时候会尝试使用 CAS 来替换 Mark Word 里面的线程 ID 为新线程的 ID,这个时候要分两种情况:
沉默王二's avatar
沉默王二 已提交
173

沉默王二's avatar
同步  
沉默王二 已提交
174 175
- 成功,表示之前的线程不存在了, Mark Word 里面的线程 ID 为新线程的 ID,锁不会升级,仍然为偏向锁;
- 失败,表示之前的线程仍然存在,那么暂停之前的线程,设置偏向锁标识为 0,并设置锁标志位为 00,升级为轻量级锁,会按照轻量级锁的方式进行竞争锁。
沉默王二's avatar
沉默王二 已提交
176

沉默王二's avatar
同步  
沉默王二 已提交
177 178 179
[CAS: Compare and Swap](https://javabetter.cn/thread/cas.html) 会在后面细讲,可戳链接直达,这里简单提一嘴。

CAS 是比较并设置的意思,用于在硬件层面上提供原子性操作。在 在某些处理器架构(如x86)中,比较并交换通过指令 CMPXCHG 实现((Compare and Exchange),一种原子指令),通过比较是否和给定的数值一致,如果一致则修改,不一致则不修改。
沉默王二's avatar
沉默王二 已提交
180 181 182

线程竞争偏向锁的过程如下:

沉默王二's avatar
同步  
沉默王二 已提交
183
![](https://cdn.tobebetterjavaer.com/stutymore/synchronized-20230728110319.png)
沉默王二's avatar
沉默王二 已提交
184

沉默王二's avatar
同步  
沉默王二 已提交
185
图中涉及到了 lock record 指针指向当前堆栈中的最近一个 lock record,是轻量级锁按照先来先服务的模式进行了轻量级锁的加锁。
沉默王二's avatar
沉默王二 已提交
186

沉默王二's avatar
同步  
沉默王二 已提交
187
### 撤销偏向锁
沉默王二's avatar
沉默王二 已提交
188

沉默王二's avatar
同步  
沉默王二 已提交
189
偏向锁使用了一种**等到竞争出现才释放锁的机制**,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。
沉默王二's avatar
沉默王二 已提交
190 191 192 193

偏向锁升级成轻量级锁时,会暂停拥有偏向锁的线程,重置偏向锁标识,这个过程看起来容易,实则开销还是很大的,大概的过程如下:

1. 在一个安全点(在这个时间点上没有字节码正在执行)停止拥有锁的线程。
沉默王二's avatar
同步  
沉默王二 已提交
194 195
2. 遍历线程栈,如果存在锁记录的话,需要修复锁记录和 Mark Word,使其变成无锁状态。
3. 唤醒被停止的线程,将当前锁升级成轻量级锁。
沉默王二's avatar
沉默王二 已提交
196 197 198 199

所以,如果应用程序里所有的锁通常处于竞争状态,那么偏向锁就会是一种累赘,对于这种情况,我们可以一开始就把偏向锁这个默认功能给关闭:

```java
沉默王二's avatar
同步  
沉默王二 已提交
200
-XX:UseBiasedLocking=false
沉默王二's avatar
沉默王二 已提交
201 202 203 204
```

下面这个经典的图总结了偏向锁的获得和撤销:

沉默王二's avatar
同步  
沉默王二 已提交
205
![](https://cdn.tobebetterjavaer.com/stutymore/synchronized-20230728112620.png)
沉默王二's avatar
沉默王二 已提交
206

沉默王二's avatar
同步  
沉默王二 已提交
207
## 轻量级锁
沉默王二's avatar
沉默王二 已提交
208

沉默王二's avatar
同步  
沉默王二 已提交
209
多个线程在不同时段获取同一把锁,即不存在锁竞争的情况,也就没有线程阻塞。针对这种情况,JVM 采用轻量级锁来避免线程的阻塞与唤醒。
沉默王二's avatar
沉默王二 已提交
210

沉默王二's avatar
同步  
沉默王二 已提交
211
JVM 会为每个线程在当前线程的栈帧中创建用于存储锁记录的空间,我们称为 Displaced Mark Word。如果一个线程获得锁的时候发现是轻量级锁,会把锁的 Mark Word 复制到自己的 Displaced Mark Word 里面。
沉默王二's avatar
沉默王二 已提交
212

沉默王二's avatar
同步  
沉默王二 已提交
213
然后线程尝试用 CAS 将锁的 Mark Word 替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示 Mark Word 已经被替换成了其他线程的锁记录,说明在与其它线程竞争锁,当前线程就尝试使用自旋来获取锁。
沉默王二's avatar
沉默王二 已提交
214 215 216

> 自旋:不断尝试去获取锁,一般用循环来实现。

沉默王二's avatar
同步  
沉默王二 已提交
217
自旋是需要消耗 CPU 的,如果一直获取不到锁的话,那该线程就一直处在自旋状态,白白浪费 CPU 资源。解决这个问题最简单的办法就是指定自旋的次数,例如让其循环 10 次,如果还没获取到锁就进入阻塞状态。
沉默王二's avatar
沉默王二 已提交
218

沉默王二's avatar
同步  
沉默王二 已提交
219
但是 JDK 采用了更聪明的方式——适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。
沉默王二's avatar
沉默王二 已提交
220

沉默王二's avatar
同步  
沉默王二 已提交
221
自旋也不是一直进行下去的,如果自旋到一定程度(和 JVM、操作系统相关),依然没有获取到锁,称为自旋失败,那么这个线程会阻塞。同时这个锁就会**升级成重量级锁**
沉默王二's avatar
沉默王二 已提交
222

沉默王二's avatar
同步  
沉默王二 已提交
223
### 轻量级锁的释放
沉默王二's avatar
沉默王二 已提交
224

沉默王二's avatar
同步  
沉默王二 已提交
225
在释放锁时,当前线程会使用 CAS 操作将 Displaced Mark Word 的内容复制回锁的 Mark Word 里面。如果没有发生竞争,那么这个复制的操作会成功。如果有其他线程因为自旋多次导致轻量级锁升级成了重量级锁,那么 CAS 操作会失败,此时会释放锁并唤醒被阻塞的线程。
沉默王二's avatar
沉默王二 已提交
226 227 228

一张图说明加锁和释放锁的过程:

沉默王二's avatar
同步  
沉默王二 已提交
229
![](https://cdn.tobebetterjavaer.com/stutymore/synchronized-20230728114101.png)
沉默王二's avatar
沉默王二 已提交
230

沉默王二's avatar
同步  
沉默王二 已提交
231
## 重量级锁
沉默王二's avatar
沉默王二 已提交
232

沉默王二's avatar
同步  
沉默王二 已提交
233
重量级锁依赖于操作系统的互斥锁(mutex,用于保证任何给定时间内,只有一个线程可以执行某一段特定的代码段) 实现,而操作系统中线程间状态的转换需要相对较长的时间,所以重量级锁效率很低,但被阻塞的线程不会消耗 CPU。
沉默王二's avatar
沉默王二 已提交
234 235 236

前面说到,每一个对象都可以当做一个锁,当多个线程同时请求某个对象锁时,对象锁会设置几种状态用来区分请求的线程:

沉默王二's avatar
同步  
沉默王二 已提交
237
- Contention List:所有请求锁的线程将被首先放置到该竞争队列
沉默王二's avatar
沉默王二 已提交
238 239 240 241
- Entry List:Contention List 中那些有资格成为候选人的线程被移到 Entry List
- Wait Set:那些调用 wait 方法被阻塞的线程被放置到 Wait Set
- OnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为 OnDeck
- Owner:获得锁的线程称为 Owner
沉默王二's avatar
同步  
沉默王二 已提交
242 243 244
- !Owner:释放锁的线程

当一个线程尝试获得锁时,如果该锁已经被占用,则会将该线程封装成一个`ObjectWaiter`对象插入到 Contention List 队列的队首,然后调用`park` 方法挂起当前线程。
沉默王二's avatar
沉默王二 已提交
245

沉默王二's avatar
同步  
沉默王二 已提交
246
当线程释放锁时,会从 Contention List 或 EntryList 中挑选一个线程唤醒,被选中的线程叫做`Heir presumptive`即假定继承人,假定继承人被唤醒后会尝试获得锁,但`synchronized`是非公平的,所以假定继承人不一定能获得锁。
沉默王二's avatar
沉默王二 已提交
247

沉默王二's avatar
同步  
沉默王二 已提交
248 249 250
这是因为对于重量级锁,线程需要先自旋尝试获得锁,这样做的目的是为了减少执行操作系统同步操作带来的开销。如果自旋不成功再进入等待队列。这对那些已经在等待队列中的线程来说,稍微显得不公平,还有一个不公平的地方是自旋线程可能会抢占了 Ready 线程的锁。

如果线程获得锁后调用`Object.wait`方法,则会将线程加入到 WaitSet 中,当被`Object.notify`唤醒后,会将线程从 WaitSet 移动到 Contention List 或 EntryList 中去。需要注意的是,当调用一个锁对象的`wait``notify`方法时,**如当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁**
沉默王二's avatar
沉默王二 已提交
251

沉默王二's avatar
同步  
沉默王二 已提交
252
## 锁的升级流程
沉默王二's avatar
沉默王二 已提交
253

沉默王二's avatar
同步  
沉默王二 已提交
254 255
每一个线程在准备获取共享资源时:
第一步,检查 MarkWord 里面是不是放的自己的 ThreadId ,如果是,表示当前线程是处于 “偏向锁” 。
沉默王二's avatar
沉默王二 已提交
256

沉默王二's avatar
同步  
沉默王二 已提交
257
第二步,如果 MarkWord 不是自己的 ThreadId,锁升级,这时候,用 CAS 来执行切换,新的线程根据 MarkWord 里面现有的 ThreadId,通知之前线程暂停,之前线程将 Markword 的内容置为空。
沉默王二's avatar
沉默王二 已提交
258

沉默王二's avatar
同步  
沉默王二 已提交
259 260
第三步,两个线程都把锁对象的 HashCode 复制到自己新建的用于存储锁的记录空间,接着开始通过 CAS 操作,
把锁对象的 MarKword 的内容修改为自己新建的记录空间的地址的方式竞争 MarkWord。
沉默王二's avatar
沉默王二 已提交
261

沉默王二's avatar
同步  
沉默王二 已提交
262
第四步,第三步中成功执行 CAS 的获得资源,失败的则进入自旋 。
沉默王二's avatar
沉默王二 已提交
263 264 265 266 267

第五步,自旋的线程在自旋过程中,成功获得资源(即之前获的资源的线程执行完成并释放了共享资源),则整个状态依然处于 轻量级锁的状态,如果自旋失败 。

第六步,进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待之前线程执行完成并唤醒自己。

沉默王二's avatar
沉默王二 已提交
268
## 小结
沉默王二's avatar
沉默王二 已提交
269

沉默王二's avatar
沉默王二 已提交
270 271 272 273 274 275 276
- Java 中的每一个对象都可以作为一个锁,Java 中的锁都是基于对象的。
- synchronized 关键字可以用来修饰方法和代码块,它可以保证在同一时刻最多只有一个线程执行该段代码。
- synchronized 关键字在修饰方法时,锁为当前实例对象;在修饰静态方法时,锁为当前 Class 对象;在修饰代码块时,锁为括号里面的对象。
- Java 6 为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁“。在 Java 6 以前,所有的锁都是”重量级“锁。所以在 Java 6 及其以后,一个对象其实有四种锁状态,它们级别由低到高依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。
- 偏向锁会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。也就是说,偏向锁在资源无竞争情况下消除了同步语句,连 CAS 操作都不做了,提高了程序的运行性能。
- 轻量级锁是通过 CAS 操作和自旋来实现的,如果自旋失败,则会升级为重量级锁。
- 重量级锁依赖于操作系统的互斥量(mutex) 实现的,而操作系统中线程间状态的转换需要相对较长的时间,所以重量级锁效率很低,但被阻塞的线程不会消耗 CPU。
沉默王二's avatar
沉默王二 已提交
277

沉默王二's avatar
沉默王二 已提交
278
>编辑:沉默王二,原文内容来源于朋友小七萤火虫开源的这个仓库:[深入浅出 Java 多线程](http://concurrent.redspider.group/),强烈推荐。
沉默王二's avatar
沉默王二 已提交
279

沉默王二's avatar
同步  
沉默王二 已提交
280
---
沉默王二's avatar
沉默王二 已提交
281

沉默王二's avatar
9000+  
沉默王二 已提交
282
GitHub 上标星 9000+ 的开源知识库《[二哥的 Java 进阶之路](https://github.com/itwanger/toBeBetterJavaer)》第一版 PDF 终于来了!包括 Java 基础语法、数组&字符串、OOP、集合框架、Java IO、异常处理、Java 新特性、网络编程、NIO、并发编程、JVM 等等,共计 32 万余字,可以说是通俗易懂、风趣幽默……详情戳:[太赞了,GitHub 上标星 9000+ 的 Java 教程](https://javabetter.cn/overview/)
沉默王二's avatar
沉默王二 已提交
283 284

微信搜 **沉默王二** 或扫描下方二维码关注二哥的原创公众号沉默王二,回复 **222** 即可免费领取。
沉默王二's avatar
沉默王二 已提交
285

沉默王二's avatar
同步  
沉默王二 已提交
286
![](https://cdn.tobebetterjavaer.com/tobebetterjavaer/images/gongzhonghao.png)