1.概述
如果两个线程,一个执行i++,另一个执行i–(i 为静态变量)
如对于 i++ 而言,实际会产生如下的 JVM 字节码指令:
getstatic i // 获取静态变量i的值 iconst_1 // 准备常量1 iadd // 自增 putstatic i // 将修改后的值存入静态变量i
而对应 i– 也是类似:
getstatic i // 获取静态变量i的值 iconst_1 // 准备常量1 isub // 自减 putstatic i // 将修改后的值存入静态变量i
如果是单线程以上 8 行代码是顺序执行(不会交错)没有问题:

但多线程下这 8 行代码可能交错运行:
比如出现负数的情况:

临界区 Critical Section
在多个线程对共享资源读写操作时发生指令交错,就会出现问题
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
竞态条件 Race Condition
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件
synchronized 解决方案
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
- 阻塞式的解决方案:synchronized,Lock
- 非阻塞式的解决方案:原子变量
synchronized:即俗称的对象锁,它采用互斥的方式让同一时刻至多只有一个线程能持有对象锁,其它线程再想获取这个对象锁时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换
注意
虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:
- 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
- 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
synchronized 块是 Java 提供的一种原子性内置锁,Java 中的每个对象都可以把它当作一个同步锁来使用,这些 Java 内置的使用者看不到的锁被称为内部锁,也叫作监视器锁。线程的执行代码在进入 synchronized 代码块前会自动获取内部锁,这时候其他线程访问该同步代码块时会被阻塞挂起。拿到内部锁的线程会在正常退出同步代码块或者抛出异常后或者在同步块内调用了该内置锁资源的 wait 系列方法时释放该内置锁。内置锁是排它锁,也就是当一个线程获取这个锁后,其他线程必须等待该线程释放锁后才能获取该锁。
synchronized语法
synchronized(对象) // 线程1, 线程2(blocked)
{
临界区
}代码实例:
package cn.itcast.test;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.T11")
public class T11 {
static int counter = 0;
static final Object room = new Object();
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (room) {
counter++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
synchronized (room) {
counter--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}",counter);
}
}
注意:
- 锁住了对象也不能一直执行下去
- 这中间即使synchronized(对象)的线程(设为t1)的 cpu 时间片不幸用完,被踢出了门外(不要错误理解为锁住了对象就能一直执行下去哦),这时门还是锁住的,仍拿着钥匙,其他线程还在阻塞状态进不来,只有下次轮到 他自己再次获得时间片时才能开门进入.
- 当 t1 执行完 synchronized{} 块内的代码,这时候才会从房间出来并解开门上的锁,才到下一个线程拿锁开门。
如图:


synchronized 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。
注意:
如果 t1 synchronized(obj) 而 t2 没有加,那,对于t2就不会被阻塞。
面向对象方法:
package cn.yutian.test;
import lombok.extern.slf4j.Slf4j;
@Slf4j(topic = "c.Test17")
public class Test17 {
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
room.increment();
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
room.decrement();
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}", room.get());
}
}
class Room {
int value = 0;
public void increment() {
synchronized (this) {
value++;
}
}
public void decrement() {
synchronized (this) {
value--;
}
}
public int get() {
synchronized (this) {
return value;
}
}
}方法上的 synchronized( synchronized只能锁对象,加在方法上锁当前对象)
class Test{
public synchronized void test() {
}
}
等价于
class Test{
public void test() {
synchronized(this) {
}
}
}
class Test{
public synchronized static void test() {
}
}
等价于
class Test{
public static void test() {
synchronized(Test.class) {// 静态方法没有this变量
}
}
}
注意;如下,一个class对象,一个实例对象,也不是同一对象,所以锁不住
@Slf4j(topic = "c.Number")
class Number{
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n1.b(); }).start();
}
而下面的锁住的是同一个对象,是class对象。
class Number{
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public static synchronized void b() {
log.debug("2");
}
}
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(()->{ n1.a(); }).start();
new Thread(()->{ n2.b(); }).start();
}
2.变量的线程安全分析
成员变量和静态变量是否线程安全:
- 如果它们没有共享,则线程安全
- 如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
- 如果只有读操作,则线程安全
- 如果有读写操作,则这段代码是临界区,需要考虑线程安全
局部变量是否线程安全
- 局部变量是线程安全的
- 但局部变量引用的对象则未必,如果该对象没有逃离方法的作用访问,它是线程安全的,如果该对象逃离方法的作用范围,需要考虑线程安全.
局部变量线程安全分析
public static void test1() {
int i = 10;
i++;
}每个线程调用 test1() 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,因此不存在共享。
如图:

如果是成员变量的引用

如果是局部变量的引用

因为是局部变量,每个线程调用时会创建其不同实例,没有共享
常见线程安全类
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrent 包下的类
这里说它们是线程安全的是指,多个线程调用它们同一个实例的某个方法时,是线程安全的。也可以理解为
- 它们的每个方法是原子的
- 但注意它们多个方法的组合不是原子的
Hashtable table = new Hashtable();
new Thread(()->{
table.put("key", "value1");
}).start();
new Thread(()->{
table.put("key", "value2");
}).start();比如上面的put方法:

线程安全类方法的组合
Hashtable table = new Hashtable();
// 线程1,线程2
if( table.get("key") == null) {
table.put("key", value);
}
不可变类线程安全性
String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此它们的方法都是线程安全的
而String 的 replace,substring 等方法改变值,那么这些方法又是如何保证线程安全的呢?
举例:
String的substring方法

新建一个字符串,对原有值复制,

也就是说其内部的值根本没改变
String要设置成final,避免被子类方法覆盖,变成线程不安全。

如下代码:
package cn.yutian.n4.exercise;
import lombok.extern.slf4j.Slf4j;
import java.util.Random;
@Slf4j(topic = "c.ExerciseTransfer")
public class ExerciseTransfer {
public static void main(String[] args) throws InterruptedException {
Account a = new Account(1000);
Account b = new Account(1000);
Thread t1 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
a.transfer(b, randomAmount());
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1000; i++) {
b.transfer(a, randomAmount());
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
// 查看转账2000次后的总金额
log.debug("total:{}", (a.getMoney() + b.getMoney()));
}
// Random 为线程安全
static Random random = new Random();
// 随机 1~100
public static int randomAmount() {
return random.nextInt(100) + 1;
}
}
// 账户
class Account {
private int money;
public Account(int money) {
this.money = money;
}
public int getMoney() {
return money;
}
public void setMoney(int money) {
this.money = money;
}
// 转账
public void transfer(Account target, int amount) {
synchronized(Account.class) {
if (this.money >= amount) {
this.setMoney(this.getMoney() - amount);
target.setMoney(target.getMoney() + amount);
}
}
}
}这样才能保证线程安全,而不能使用this
如下,线程不安全

Monitor 概念
Java 对象头
以 32 位虚拟机为例
普通对象(klass word是指向class的指针)

数组对象

其中 Mark Word 结构为

3.Monitor原理
Monitor 被翻译为监视器或管程
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针
Monitor 结构如下

- 刚开始 Monitor 中 Owner 为 null
- 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一个 Owner
- 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行synchronized(obj),就会进入EntryList BLOCKED
- Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的(jdk底层实现)
- 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲wait-notify 时会分析
synchronized 必须是进入同一个对象的 monitor 才有上述的效果
不加 synchronized 的对象不会关联监视器,不遵从以上规则
4.synchronized 原理
示例:
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}
对应的字节码解释为
0: getstatic #2 // <- lock引用 (synchronized开始) 3: dup 4: astore_1 // lock引用 -> slot 1 5: monitorenter // 将 lock对象 MarkWord 置为 Monitor 指针 6: getstatic #3 // <- i 9: iconst_1 // 准备常数 1 10: iadd // +1 11: putstatic #3 // -> i 14: aload_1 // <- lock引用 15: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList 16: goto 24 --------到24前为异常表内容。 19: astore_2 // e -> slot 2 20: aload_1 // <- lock引用 21: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList 22: aload_2 // <- slot 2 (e) 23: athrow // throw e 24: return
异常表:

在这些指令中,你将发现 monitorenter和monitorexit是成对出现的(有些时候会出现一个monitorenter多个monitor exit,但是每一个monitor exit之前必有对应的monitorenter。
(1) Monitorenter
每个对象都与一个 monitor 相关联,一个 monitor 的 lock 的锁只能被一个线程在同一时间获得,在一个线程尝试获得与对象关联 monitor的所有权时会发生如下的几件事情。
- 如果 monitor 的计数器为 0,则意味着该monitor 的lock还没有被获得,某个线程获得之后将立即对该计数器加一,从此该线程就是这个monitor的所有者了。
- 如果一个已经拥有该monitor所有权的线程重入,则会导致monitor计数器再次累加。
- 如果monitor已经被其他线程所拥有,则其他线程尝试获取该monitor的所有权时,会被陷入阻塞状态直到monitor计数器变为0,才能再次尝试获取对monitor的所有权。
(2) Monitorexit
释放对monitor的所有权,想要释放对某个对象关联的monitor的所有权的前提是,你曾经获得了所有权。释放monitor所有权的过程比较简单,就是将monitor的计数器减一,如果计数器的结果为0,那就意味着该线程不再拥有对该monitor的所有权,通俗地讲就是解锁。与此同时被该monitor block的线程将再次尝试获得对该monitor的所有权。
5.再次介绍 synchronized
官网介绍:Synchronization (The Java™ Tutorials > Essential Java Classes > Concurrency) (oracle.com)
synchronized关键字可以实现一个简单的策略来防止线程干扰和内存一致性错误,如果一个对象对多个线程是可见的,那么对该对象的所有读或者写都将通过同步的方式来进行,具体表现如下:
- synchronized关键字提供了一种锁的机制,能够确保共享变量的互斥访问,从而防止数据不一致问题的出现。
- synchronized关键字包括 monitorenter和 monitorexit 两个JVM指令,它能够保证在任何时候任何线程执行到 monitorenter成功之前都必须从主内存中获取数据,而不是从缓存中,在monitorexit运行成功之后,共享变量被更新后的值必须刷入主内存
- synchronized 的指令严格遵守 java happens-before 规则,一个 monitorexit 指令之前必定要有一个monitorenter。
使用 synchronized 需要注意的问题:
- 与 monitor 关联的对象不能为空,如:synchronized(null)错误
- synchronized 作用域太大
- 不同的 monitor企图锁相同的方法
- 多个锁的交叉导致死锁
- 使用synchronized关键字同步类的不同实例方法,争抢的是同一个monitor的lock,运行程序只有一个方法被调用,另外一个方法根本没有被调用。
1. 轻量级锁
轻量级锁的使用场景:
如果一个对象虽然有多线程要加锁,但加锁的时间是错开的(也就是没有竞争),那么可以使用轻量级锁来优化。轻量级锁对使用者是透明的,即语法仍然是 synchronized
假设有两个方法同步块,利用同一个对象加锁
static final Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块 A
method2();
}
}
public static void method2() {
synchronized( obj ) {
// 同步块 B
}
}
1.创建锁记录( Lock Record )对象,每个线程都的栈帧都会包含一个锁记录的结构,内部可以存储锁定对象的Mark Word

2.让锁记录中Object reference指向锁对象,并尝试用cas替换Object的Mark Word,将Mark Word的值存入锁记录

3.如果 cas替换成功,对象头中存储了 锁记录地址和状态 00,表示由该线程给对象加锁,这时图示如下

4.如果 cas 失败,有两种情况
- 如果是其它线程已经持有了该 Object 的轻量级锁,这时表明有竞争,进入锁膨胀过程
- 如果是自己执行了 synchronized 锁重入(当前线程的另一条锁记录·),那么再添加一条 Lock Record 作为重入的计数

5.当退出 synchronized 代码块(解锁时)如果有取值为 null的锁记录,表示有重入,这时重置锁记录,表示重入计数减一。

6.当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头
- 成功,则解锁成功
- 失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程
2. 锁膨胀
如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
static Object obj = new Object();
public static void method1() {
synchronized( obj ) {
// 同步块
}
}
当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁

这时 Thread-1 加轻量级锁失败,进入锁膨胀流程
- 即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址
- 然后自己进入 Monitor 的 EntryList BLOCKED

当 Thread-0 退出同步块解锁时,使用 cas 将 Mark Word 的值恢复给对象头,失败。这时会进入重量级解锁流程,即按照 Monitor 地址找到 Monitor 对象,设置 Owner 为 null,唤醒 EntryList 中 BLOCKED 线程。
3. 自旋优化
重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。
自旋重试成功的情况(多核)

自旋重试失败的情况

注意:
- 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。
- 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。
- Java 7 之后不能控制是否开启自旋功能。
4. 偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。
Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有。
例如:
static final Object obj = new Object();
public static void m1() {
synchronized( obj ) {
// 同步块 A
m2();
}
}
public static void m2() {
synchronized( obj ) {
// 同步块 B
m3();
}
}
public static void m3() {
synchronized( obj ) {
// 同步块 C
}
}


偏向状态
回忆一下对象头格式

Java对象头的组成
对象头存储内容图例:
如thread:54表示持有偏向锁的线程ID和其他信息占54位

- lock(01或00那): 锁状态标记位,该标记的值不同,整个mark word表示的含义不同。
- biased_lock:偏向锁标记,为1时表示对象启用偏向锁,为0时表示对象没有偏向锁
- age:Java GC标记位对象年龄。
- identity_hashcode:对象标识Hash码,采用延迟加载技术。当对象使用HashCode()计算后,并会将结果写到该对象头中。当对象被锁定时,该值会移动到线程Monitor中。
- thread:持有偏向锁的线程ID和其他信息。这个线程ID并不是JVM分配的线程ID号,和Java Thread中的ID是两个概念。
- epoch:偏向时间戳。
- ptr_to_lock_record:指向栈中锁记录的指针。
- ptr_to_heavyweight_monitor:指向线程Monitor的指针。
其中lock和biased_lock:

一个对象创建时:
- 如果开启了偏向锁(默认开启),那么对象创建后,markword 值为 0x05 即最后 3 位为 101,这时它的thread、epoch、age 都为 0
- 偏向锁是默认是延迟的,不会在程序启动时立即生效,如果想避免延迟,可以加 VM 参数 –
XX:BiasedLockingStartupDelay=0 来禁用延迟 - 如果没有开启偏向锁,那么对象创建后,markword 值为 0x01 即最后 3 位为 001,这时它的 hashcode、age 都为 0,第一次用到 hashcode 时才会赋值
加入jol,利用 jol 第三方工具来查看对象头信息(注意这里我扩展了 jol 让它输出更为简洁)
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
1.测试偏向锁(偏向锁是默认是延迟的,不会在程序启动时立即生效,所以不用VM参数就sleep)
private static void test1() {
Dog d = new Dog();
log.debug(ClassLayout.parseInstance(d).toPrintable());
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug(ClassLayout.parseInstance(new Dog()).toPrintable());
}

unused:1 | age:4 | biased_lock:1 | lock:2 0 0000 0 01 代表A对象正处于无锁状态
unused:1 | age:4 | biased_lock:1 | lock:2 0 0000 1 01 代表A对象正处于偏向状态
注意:
这里没使用synchronized关键字,此时 thread 和 epoch 的 位置的均为0,说明当前偏向锁并没有偏向任何线程。此时这个偏向锁正处于可偏向状态,只是准备好进行偏向了,可以理解为此时的偏向锁是一个特殊状态的无锁。
public static void main(String[] args) throws IOException, InterruptedException {
Dog d = new Dog();
log.debug(ClassLayout.parseInstance(d).toPrintable());
synchronized (d){
log.debug(ClassLayout.parseInstance(d).toPrintable());
}
log.debug(ClassLayout.parseInstance(d).toPrintable());
}此时对象a,对象头内容有了明显的变化,当前偏向锁偏向主线程。

2.测试禁用
如果该对象是要在多线程下被竞争访问,可以代码运行时在添加 VM 参数禁用偏向锁
-XX:-UseBiasedLocking
禁用偏向锁后,使用轻量级锁

所以,使用优先级为:
- 偏向锁优先
- 而当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
- 轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。
3.测试 hashCode
正常状态对象一开始是没有 hashCode 的,第一次调用才生成
- 调用了对象的 hashCode,但偏向锁的对象 MarkWord 中存储的是线程 id,如果调用hashCode 会导致偏向锁被撤销,存储了对象的 hashCode,存不下偏向锁了,没有另外的存储空间。轻量级锁和重量级锁有存储空间。
- 轻量级锁可以在锁记录中记录 hashCode
- 重量级锁会在 Monitor 中记录 hashCode
- 在调用 hashCode 后使用偏向锁,记得去掉 -XX:-UseBiasedLocking
public static void main(String[] args) throws IOException, InterruptedException {
Thread.sleep(5000);
Dog d = new Dog();
d.hashCode();
log.debug(ClassLayout.parseInstance(d).toPrintable());
synchronized (d){
log.debug(ClassLayout.parseInstance(d).toPrintable());
}
log.debug(ClassLayout.parseInstance(d).toPrintable());
}
当然,看得出来,使用得是轻量级锁,因为轻量级锁会在锁记录中记录 hashCode
注意:
- 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
- 调用 wait/notify,会升级为重量级锁
- 如果对象虽然被多个线程访问,但没有竞争,这时偏向了线程 T1 的对象仍有机会重新偏向 T2,重偏向会重置对象的 Thread ID,撤销偏向锁而变成轻量级锁,但不是一下子全部变成轻量级锁,而是不断每个对象每个对象的撤销偏向锁而变成轻量级锁,而且当撤销偏向锁阈值超过 20 次后,jvm 会觉得是不是偏向错了,于是会在给这些对象加锁时重新偏向至加锁线程即T2(批量重偏向)。
- 当撤销偏向锁阈值超过 40 次后,jvm 会觉得确实偏向错了,根本就不该偏向。于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的(变成001)
锁消除
锁消除即删除不必要的加锁操作。JVM在运行时,对一些“在代码上要求同步,但是被检测到不可能存在共享数据竞争情况”的锁进行消除。根据代码逃逸技术,如果判断到一段代码中,堆上的数据不会逃逸出当前线程,那么就可以认为这段代码是线程安全的,无需加锁。
锁粗化
假设一系列的连续操作都会对同一个对象反复加锁及解锁,甚至加锁操作是出现在循环体中的,即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。如果JVM检测到有一连串零碎的操作都是对同一对象的加锁,将会扩大加锁同步的范围(即锁粗化)到整个操作序列的外部。比如jdk内部会对这类代码锁的请求做一些优化,直接把加锁代码写在循环体的外面,这样一次锁的请求就可以达到我们的要求。
6.活跃性
将锁的粒度细分
好处,是可以增强并发度
坏处,如果一个线程需要同时获得多把锁,就容易发生死锁
死锁
一个线程需要同时获取多把锁,这时就容易发生死锁
t1 线程 获得A对象 锁,接下来想获取B对象的锁 t2 线程获得B对象锁,接下来想获取 A对象 的锁 例:
package cn.yutian.n4.deadlock;
import lombok.extern.slf4j.Slf4j;
import static cn.yutian.n2.util.Sleeper.sleep;
@Slf4j(topic = "c.TestDeadLock")
public class TestDeadLock {
public static void main(String[] args) {
test1();
}
private static void test1() {
Object A = new Object();
Object B = new Object();
Thread t1 = new Thread(() -> {
synchronized (A) {
log.debug("lock A");
sleep(1);
synchronized (B) {
log.debug("lock B");
log.debug("操作...");
}
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized (B) {
log.debug("lock B");
sleep(0.5);
synchronized (A) {
log.debug("lock A");
log.debug("操作...");
}
}
}, "t2");
t1.start();
t2.start();
}
}

检测死锁可以使用 jconsole工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁:
这里就不演示了,可以看看我的博客
JVM性能监控与调优篇笔记 – Dreams (tanjy.site)
活锁
活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束,例如:
package cn.yutian.n4;
import lombok.extern.slf4j.Slf4j;
import static cn.yutian.n2.util.Sleeper.sleep;
@Slf4j(topic = "c.TestLiveLock")
public class TestLiveLock {
static volatile int count = 10;
static final Object lock = new Object();
public static void main(String[] args) {
new Thread(() -> {
// 期望减到 0 退出循环
while (count > 0) {
sleep(0.2);
count--;
log.debug("count: {}", count);
}
}, "t1").start();
new Thread(() -> {
// 期望超过 20 退出循环
while (count < 20) {
sleep(0.2);
count++;
log.debug("count: {}", count);
}
}, "t2").start();
}
}
饥饿
一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束
先来看看使用顺序加锁的方式解决之前的死锁问题

顺序加锁的解决方案

参考资料
黑马程序员深入学习Java并发编程:https://www.bilibili.com/video/BV16J411h7Rd
汪文君著《Java高并发编程详解》
翟陆续著《Java并发编程之美》
盘一盘 synchronized (一)—— 从打印Java对象头说起 – 柠檬五个半 – 博客园 (cnblogs.com)


