Young Kbt blog Young Kbt blog
首页
  • java基础

    • Java基础
    • Java集合
    • Java反射
    • JavaJUC
    • JavaJVM
  • Java容器

    • JavaWeb
  • Java版本新特性

    • Java新特性
  • SQL 数据库

    • MySQL
    • Oracle
  • NoSQL 数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • ActiveMQ
    • RabbitMQ
    • RocketMQ
    • Kafka
  • 进阶服务

    • Nginx
  • Spring
  • Spring Boot
  • Spring Security
  • 设计模式
  • 算法
  • 知识
  • 管理

    • Maven
    • Git
  • 部署

    • Linux
    • Docker
    • Jenkins
    • Kubernetes
  • 进阶

    • TypeScript
  • 框架

    • React
    • Vue2
    • Vue3
  • 轮子工具
  • 项目工程
  • 友情链接
  • 本站

    • 分类
    • 标签
    • 归档
  • 我的

    • 收藏
    • 关于
    • Vue2-Admin (opens new window)
    • Vue3-Admin(完善) (opens new window)
GitHub (opens new window)

Shp Liu

朝圣的使徒,正在走向编程的至高殿堂!
首页
  • java基础

    • Java基础
    • Java集合
    • Java反射
    • JavaJUC
    • JavaJVM
  • Java容器

    • JavaWeb
  • Java版本新特性

    • Java新特性
  • SQL 数据库

    • MySQL
    • Oracle
  • NoSQL 数据库

    • Redis
    • ElasticSearch
  • 数据库

    • MyBatis
    • MyBatis-Plus
  • 消息中间件

    • ActiveMQ
    • RabbitMQ
    • RocketMQ
    • Kafka
  • 进阶服务

    • Nginx
  • Spring
  • Spring Boot
  • Spring Security
  • 设计模式
  • 算法
  • 知识
  • 管理

    • Maven
    • Git
  • 部署

    • Linux
    • Docker
    • Jenkins
    • Kubernetes
  • 进阶

    • TypeScript
  • 框架

    • React
    • Vue2
    • Vue3
  • 轮子工具
  • 项目工程
  • 友情链接
  • 本站

    • 分类
    • 标签
    • 归档
  • 我的

    • 收藏
    • 关于
    • Vue2-Admin (opens new window)
    • Vue3-Admin(完善) (opens new window)
GitHub (opens new window)
  • Java 基础 - SE

  • Java 进阶 - SE

  • Java 集合 - Collection

  • Java 并发 - JUC

    • JUC - 概念
    • JUC - Java线程
    • JUC - 共享模型之管程安全
    • JUC - 共享模式之锁基础
    • JUC - 共享模式之锁进阶
      • Park & Unpark
        • 基本使用
        • 特点
        • park 和 unpark 原理
      • 重新理解线程状态转换
        • 多种情况
        • 总结
      • 多把锁
        • 多把不相干的锁
        • 锁粒度细分优缺点
      • 活跃性
        • 死锁
        • 定位死锁
        • 哲学家就餐问题
        • 活锁
        • 饥饿
        • 公平锁非公平锁
        • 可重入锁
        • 自旋锁
      • ReentrantLock
        • 可重入
        • 可打断
        • 锁超时
        • 使用 tryLock 解决哲学家就餐问题
        • 公平锁
        • 条件变量
        • 区别
      • 同步模式之顺序控制
        • 固定运行顺序
        • wait notify 版
        • Park Unpark 版
        • 交替输出
        • wait notify 版
        • Lock 条件变量版
        • Park Unpark 版
    • JUC - 共享模式之内存
    • JUC - 共享模型之无锁
    • JUC - 共享模型之不可变
    • JUC - 共享模型之线程池
    • JUC - 共享模型之读写锁
    • JUC - 共享模式之并发类
  • Java 容器 - Web

  • Java 底层 - JVM

  • Java 版本 - 新特性

  • Java
  • Java 并发 - JUC
Young Kbt
2022-05-12
8.3k
36.5m
54
目录
Park & Unpark
基本使用
特点
park 和 unpark 原理
重新理解线程状态转换
多种情况
总结
多把锁
多把不相干的锁
锁粒度细分优缺点
活跃性
死锁
定位死锁
哲学家就餐问题
活锁
饥饿
公平锁非公平锁
可重入锁
自旋锁
ReentrantLock
可重入
可打断
锁超时
使用 tryLock 解决哲学家就餐问题
公平锁
条件变量
区别
同步模式之顺序控制
固定运行顺序
wait notify 版
Park Unpark 版
交替输出
wait notify 版
Lock 条件变量版
Park Unpark 版

JUC - 共享模式之锁进阶

  • Park & Unpark
    • 基本使用
    • 特点
    • park 和 unpark 原理
  • 重新理解线程状态转换
    • 多种情况
    • 总结
  • 多把锁
    • 多把不相干的锁
    • 锁粒度细分优缺点
  • 活跃性
    • 死锁
    • 活锁
    • 饥饿
    • 公平锁非公平锁
    • 可重入锁
    • 自旋锁
  • ReentrantLock
    • 可重入
    • 可打断
    • 锁超时
    • 使用 tryLock 解决哲学家就餐问题
    • 公平锁
    • 条件变量
    • 区别
  • 同步模式之顺序控制
    • 固定运行顺序
    • 交替输出

# Park & Unpark

# 基本使用

它们是 LockSupport 类中的方法。

// 暂停当前线程
LockSupport.park(); 
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)
1
2
3
4

先 park 再 unpark

public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
        log.debug("start...");
        sleep(1);
        log.debug("park...");
        LockSupport.park();
        log.debug("resume...");
    },"t1");
    t1.start();
    sleep(2);
    log.debug("unpark...");
    LockSupport.unpark(t1);
}
1
2
3
4
5
6
7
8
9
10
11
12
13

输出:

18:42:52.585 c.TestParkUnpark [t1] - start... 
18:42:53.589 c.TestParkUnpark [t1] - park... 
18:42:54.583 c.TestParkUnpark [main] - unpark... 
18:42:54.583 c.TestParkUnpark [t1] - resume...
1
2
3
4

park 会让线程暂停,unpark 取消 park 的效果,让线程继续运行。

先 unpark 再 park

public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
        log.debug("start...");
        sleep(2);
        log.debug("park...");
        LockSupport.park();
        log.debug("resume...");
    }, "t1");
    t1.start();
    sleep(1);
    log.debug("unpark...");
    LockSupport.unpark(t1);
}
1
2
3
4
5
6
7
8
9
10
11
12
13

输出:

18:43:50.765 c.TestParkUnpark [t1] - start... 
18:43:51.764 c.TestParkUnpark [main] - unpark... 
18:43:52.769 c.TestParkUnpark [t1] - park... 
18:43:52.769 c.TestParkUnpark [t1] - resume...
1
2
3
4

所以先 unpark,再 park,那么不会暂停线程。因为 unpark 内部事先已经准备了一个「东西」让后面的 park 失效(只是后面的第一个 park,如果再调用一次 park,则线程会暂停)。

# 特点

与 Object 的 wait & notify 相比:

  • wait & notify 和 notifyAll 必须配合 Object Monitor 一起使用,而 park,unpark 不必
  • park & unpark 是以线程为单位来「阻塞」和「唤醒」线程,而 notify 只能随机唤醒一个等待线程,notifyAll 是唤醒所有等待线程,就不那么「精确」
  • park & unpark 可以先 unpark,而 wait & notify 不能先 notify

# park 和 unpark 原理

每个线程都有自己的一个 Parker 对象,由三部分组成 _counter, _cond 和 _mutex,

_counter 用来判断是否暂停线程(0 暂停,1 不暂停), _cond 是存放暂停线程的空间, _mutex 是互斥锁,打个比喻:

  • 线程就像一个旅人,Parker 就像他随身携带的背包,条件变量就好比背包中的帐篷。_counter 就好比背包中的备用干粮(0 为耗尽,1 为充足)
  • 调用 park 就是要看需不需要停下来歇息
    • 如果备用干粮耗尽,那么钻进帐篷歇息
    • 如果备用干粮充足,那么不需停留,继续前进
  • 调用 unpark,就好比令干粮充足
    • 如果这时线程还在帐篷,就唤醒让他继续前进
    • 如果这时线程还在运行,那么下次他调用 park 时,仅是消耗掉备用干粮,不需停留继续前进
      • 因为背包空间有限,多次调用 unpark 仅会补充一份备用干粮

调用 park

image-20220511230627670

  • 当前线程调用 Unsafe.park() 方法
  • 检查 _counter,本情况为 0,这时获得 _mutex 互斥锁
  • 线程进入 _cond 条件变量,变成阻塞状态
  • 设置 _counter = 0,代表暂停线程

调用 unpark

image-20220511230848615

  • 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1,代表恢复线程
  • 唤醒 _cond 条件变量中的 Thread_0
  • Thread_0 恢复运行
  • 设置 _counter 为 0,代表下一次调用 park 的线程暂停

先调用 unpark,再调用 park

image-20220511230931925

  • 调用 Unsafe.unpark(Thread_0) 方法,设置 _counter 为 1,代表恢复线程,这里指的是下一个调用 park 的线程直接恢复运行
  • 当前线程调用 Unsafe.park() 方法
  • 检查 _counter,此时为 1,这时线程无需阻塞,继续运行
  • 设置 _counter 为 0,代表下一次调用 park 的线程暂停

# 重新理解线程状态转换

image-20220508012802238

# 多种情况

假设有线程 Thread t。

情况 1 NEW --> RUNNABLE

当调用 t.start() 方法时,由 NEW --> RUNNABLE

情况 2 RUNNABLE <--> WAITING

t 线程用 synchronized(obj) 获取了对象锁后

  • 调用 obj.wait() 方法时,t 线程从 RUNNABLE --> WAITING
  • 调用 obj.notify(),obj.notifyAll(),t.interrupt() 时
    • 竞争锁成功,t 线程从 WAITING --> RUNNABLE
    • 竞争锁失败,t 线程从 WAITING --> BLOCKED
public class TestWaitNotify {
    final static Object obj = new Object();
    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码...."); // 断点
            }
        },"t1").start();
        new Thread(() -> {
            synchronized (obj) {
                log.debug("执行....");
                try {
                    obj.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.debug("其它代码...."); // 断点
            }
        },"t2").start();

        sleep(0.5);
        log.debug("唤醒 obj 上其它线程");
        synchronized (obj) {
            obj.notifyAll(); // 唤醒obj上所有等待线程 断点
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

情况 3 RUNNABLE <--> WAITING

  • 当前线程调用 t.join() 方法时,当前线程从 RUNNABLE --> WAITING
    • 注意是当前线程在 t 线程对象的监视器上等待
  • t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 WAITING --> RUNNABLE

情况 4 RUNNABLE <--> WAITING

  • 当前线程调用 LockSupport.park() 方法会让当前线程从 RUNNABLE --> WAITING
  • 调用 LockSupport.unpark(目标线程) 或调用了线程的 interrupt(),会让目标线程从 WAITING --> RUNNABLE

情况 5 RUNNABLE <--> TIMED_WAITING

t 线程用 synchronized(obj) 获取了对象锁后

  • 调用 obj.wait(long n) 方法时,t 线程从 RUNNABLE --> TIMED_WAITING
  • t 线程等待时间超过了 n 毫秒,或调用 obj.notify(),obj.notifyAll(),t.interrupt() 时
    • 竞争锁成功,t 线程从 TIMED_WAITING --> RUNNABLE
    • 竞争锁失败,t 线程从 TIMED_WAITING --> BLOCKED

情况 6 RUNNABLE <--> TIMED_WAITING

  • 当前线程调用 t.join(long n) 方法时,当前线程从 RUNNABLE --> TIMED_WAITING
    • 注意是当前线程在 t 线程对象的监视器上等待
  • 当前线程等待时间超过了 n 毫秒,或 t 线程运行结束,或调用了当前线程的 interrupt() 时,当前线程从 TIMED_WAITING --> RUNNABLE

情况 7 RUNNABLE <--> TIMED_WAITING

  • 当前线程调用 Thread.sleep(long n),当前线程从 RUNNABLE --> TIMED_WAITING
  • 当前线程等待时间超过了 n 毫秒,当前线程从 TIMED_WAITING --> RUNNABLE

情况 8 RUNNABLE <--> TIMED_WAITING

  • 当前线程调用 LockSupport.parkNanos(long nanos) 或 LockSupport.parkUntil(long millis) 时,当前线程从 RUNNABLE --> TIMED_WAITING
  • 调用 LockSupport.unpark(目标线程) 或调用了线程的 interrupt(),或是等待超时,会让目标线程从 TIMED_WAITING--> RUNNABLE

情况 9 RUNNABLE <--> BLOCKED

  • t 线程用 synchronized(obj) 获取了对象锁时如果竞争失败,从 RUNNABLE --> BLOCKED
  • 持 obj 锁线程的同步代码块执行完毕,会唤醒该对象上所有 BLOCKED 的线程重新竞争,如果其中 t 线程竞争成功,从 BLOCKED --> RUNNABLE ,其它失败的线程仍然 BLOCKED

情况 10 RUNNABLE <--> TERMINATED

当前线程所有代码运行完毕,进入 TERMINATED。

# 总结

  • 任意线程调用 wait、join、park 等多线程方法都会进入 WAITING 状态
  • 任意线程竞争抢锁失败,进入 BLOCKED 状态,竞争抢锁成功,则进入 RUNNABLE 状态
  • 任意线程调用带有时间参数的多线程方法,如 wait(long n)、join(long n)、sleep(long n)、parkUntil(long millis) 等,都进入 TIMED_WAITING 状态
  • 任意线程抢到锁后,都直接进入 RUNNABLE 状态

# 多把锁

# 多把不相干的锁

一间大屋子有两个功能:睡觉、学习,互不相干。

现在小南要学习,小女要睡觉,但如果只用一间屋子(一个对象锁)的话,那么并发度很低。

例如:

class BigRoom {
    public void sleep() {
        synchronized (this) {
            log.debug("sleeping 2 小时");
            // 睡 2s
            Sleeper.sleep(2);
        }
    }
    public void study() {
        synchronized (this) {
            log.debug("study 1 小时");
            // 睡 1s
            Sleeper.sleep(1);
        }
    }
}
public class Test {
    public static void main(String[] args) {
        BigRoom bigRoom = new BigRoom();
        
        new Thread(() -> {
            bigRoom.compute();
        },"小南").start();
        
        new Thread(() -> {
            bigRoom.sleep();
        },"小女").start();
    }   
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

某次执行结果:

12:13:54.471 [小南] c.BigRoom - study 1 小时
12:13:55.476 [小女] c.BigRoom - sleeping 2 小时
1
2

解决方法:准备多个房间(多个对象锁)。

改进代码:



 
 

























class BigRoom {
    // 多了两把锁
    private final Object studyRoom = new Object();
    private final Object bedRoom = new Object();
    public void sleep() {
        synchronized (bedRoom) {
            log.debug("sleeping 2 小时");
            Sleeper.sleep(2);
        }
    }
    public void study() {
        synchronized (studyRoom) {
            log.debug("study 1 小时");
            Sleeper.sleep(1);
        }
    }
}
public class Test {
    public static void main(String[] args) {
        BigRoom bigRoom = new BigRoom();
        new Thread(() -> {
            bigRoom.compute();
        },"小南").start();
        new Thread(() -> {
            bigRoom.sleep();
        },"小女").start();
    }   
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

某次执行结果:

12:15:35.069 [小南] c.BigRoom - study 1 小时
12:15:35.069 [小女] c.BigRoom - sleeping 2 小时
1
2

# 锁粒度细分优缺点

  • 优点:是可以增强并发度
  • 缺点:如果一个线程需要同时获得多把锁,就容易发生死锁

# 活跃性

# 死锁

有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁。

  • t1 线程获得 A 对象锁,接下来想获取 B 对象的锁
  • t2 线程获得 B 对象锁,接下来想获取 A 对象的锁

例:

public static void main(String[] args) {
    Object A = new Object();
    Object B = new Object();
    
    Thread t1 = new Thread(() -> {
        synchronized (A) {
            log.debug("lock A");
            sleep(1);
            // 试图获取 B 的锁
            synchronized (B) {
                log.debug("lock B");
                log.debug("操作...");
            }
        }
    }, "t1");
    
    Thread t2 = new Thread(() -> {
        synchronized (B) {
            log.debug("lock B");
            sleep(0.5);
            // 试图获取 A 的锁
            synchronized (A) {
                log.debug("lock A");
            }
            log.debug("操作...");
        }
    }, "t2");
    t1.start();
    t2.start();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

输出:

12:22:06.962 [t2] c.TestDeadLock - lock B 
12:22:06.962 [t1] c.TestDeadLock - lock A
// 卡在这里, 不向下运行
1
2
3

# 定位死锁

检测死锁可以使用 jconsole 工具,或者使用 jps 定位进程 id,再用 jstack 定位死锁:

cmd > jps
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
12320 Jps
22816 KotlinCompileDaemon
33200 TestDeadLock // JVM 进程
11508 Main
28468 Launcher
1
2
3
4
5
6
7

输出(截取部分):

Found one Java-level deadlock:  // 发现一个 Java 级别的死锁
=============================
"Thread-1":
 waiting to lock monitor 0x000000000361d378 (object 0x000000076b5bf1c0, a java.lang.Object),
 which is held by "Thread-0"
"Thread-0":
 waiting to lock monitor 0x000000000361e768 (object 0x000000076b5bf1d0, a java.lang.Object),
 which is held by "Thread-1"
1
2
3
4
5
6
7
8
  • 避免死锁要注意加锁顺序
  • 另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到 CPU 占用高的 Java 进程,再利用 top -Hp 进程 id 来定位是哪个线程,最后再用 jstack 排查

# 哲学家就餐问题

image-20220512005451474

有五位哲学家,围坐在圆桌旁。

  • 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考
  • 吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子
  • 如果筷子被身边的人拿着,自己就得等待

假设五位哲学家分别拿起一个筷子,且不放下,则五位哲学家就一直等待其他人放下筷子,但是其他人不放下,则僵持产生死锁。

筷子类

class Chopstick {
    String name;
    public Chopstick(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "筷子{" + name + '}';
    }
}
1
2
3
4
5
6
7
8
9
10

哲学家类

class Philosopher extends Thread {
    Chopstick left;
    Chopstick right;
    public Philosopher(String name, Chopstick left, Chopstick right) {
        super(name);
        this.left = left;
        this.right = right;
    }
    private void eat() {
        log.debug("eating...");
        Sleeper.sleep(1);
    }

    @Override
    public void run() {
        while (true) {
            // 获得左手筷子
            synchronized (left) {
                // 获得右手筷子
                synchronized (right) {
                    // 吃饭
                    eat();
                }
                // 放下右手筷子
            }
            // 放下左手筷子
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

就餐测试

public class Test {
    public static void main(String[] args) {
        Chopstick c1 = new Chopstick("1");
        Chopstick c2 = new Chopstick("2");
        Chopstick c3 = new Chopstick("3");
        Chopstick c4 = new Chopstick("4");
        Chopstick c5 = new Chopstick("5");
        new Philosopher("苏格拉底", c1, c2).start();
        new Philosopher("柏拉图", c2, c3).start();
        new Philosopher("亚里士多德", c3, c4).start();
        new Philosopher("赫拉克利特", c4, c5).start();
        new Philosopher("阿基米德", c5, c1).start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

输出结果(截取死锁部分):

12:33:15.575 [苏格拉底] c.Philosopher - eating... 
12:33:15.575 [亚里士多德] c.Philosopher - eating... 
12:33:16.580 [阿基米德] c.Philosopher - eating... 
12:33:17.580 [阿基米德] c.Philosopher - eating... 
// 卡在这里, 不向下运行
1
2
3
4
5

使用 jconsole 检测死锁,发现:

-------------------------------------------------------------------------
名称: 阿基米德
状态: cn.itcast.Chopstick@1540e19d (筷子1) 上的BLOCKED, 拥有者: 苏格拉底
总阻止数: 2, 总等待数: 1
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
 - 已锁定 cn.itcast.Chopstick@6d6f6e28 (筷子5)
-------------------------------------------------------------------------
名称: 苏格拉底
状态: cn.itcast.Chopstick@677327b6 (筷子2) 上的BLOCKED, 拥有者: 柏拉图
总阻止数: 2, 总等待数: 1
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
 - 已锁定 cn.itcast.Chopstick@1540e19d (筷子1)
-------------------------------------------------------------------------
名称: 柏拉图
状态: cn.itcast.Chopstick@14ae5a5 (筷子3) 上的BLOCKED, 拥有者: 亚里士多德
总阻止数: 2, 总等待数: 0
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
 - 已锁定 cn.itcast.Chopstick@677327b6 (筷子2)
-------------------------------------------------------------------------
名称: 亚里士多德
状态: cn.itcast.Chopstick@7f31245a (筷子4) 上的BLOCKED, 拥有者: 赫拉克利特
总阻止数: 1, 总等待数: 1
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
 - 已锁定 cn.itcast.Chopstick@14ae5a5 (筷子3)
-------------------------------------------------------------------------
名称: 赫拉克利特
状态: cn.itcast.Chopstick@6d6f6e28 (筷子5) 上的BLOCKED, 拥有者: 阿基米德
总阻止数: 2, 总等待数: 0
堆栈跟踪:
cn.itcast.Philosopher.run(TestDinner.java:48)
 - 已锁定 cn.itcast.Chopstick@7f31245a (筷子4)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

这种线程没有按预期结束,执行不下去的情况,归类为「活跃性」问题,除了死锁以外,还有活锁和饥饿者两种情况。

# 活锁

活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束,例如:

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();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

线程 t1 对 count++,线程 t2 对 count--,导致 count 加完就减,减完就加,所以 一直维持在一定范围内,导致无法结束运行。

解决:让线程进行 sleep 睡眠,其睡眠时间不能一致,可以利用 Random 产生随机睡眠时间。

# 饥饿

饥饿定义:一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,饥饿的情况不易演示,讲读写锁时会涉及饥饿问题。

如下图,线程 2 打算获取对象 A 的锁,却被线程 1 获取了,当线程 2 放弃对象 A 的锁,去获取对象 B 的锁,又被线程 1 抢先一步获取,导致线程 2 产生饥饿问题,即在整个运行过程,都无法获取任何锁。

image-20220512010215250

可以在 哲学家就餐问题 将就餐测试代码改为如下,就可以看到饥饿问题,即某个哲学家永远无法获取筷子,并且又不会产生死锁或活锁:

public class Test {
    public static void main(String[] args) {
        Chopstick c1 = new Chopstick("1");
        Chopstick c2 = new Chopstick("2");
        Chopstick c3 = new Chopstick("3");
        Chopstick c4 = new Chopstick("4");
        Chopstick c5 = new Chopstick("5");
        new Philosopher("苏格拉底", c1, c2).start();
        new Philosopher("柏拉图", c2, c3).start();
        new Philosopher("亚里士多德", c3, c4).start();
        new Philosopher("赫拉克利特", c4, c5).start();
        new Philosopher("阿基米德", c1, c5).start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

顺序加锁产生饥饿问题的解决方案:在线程 2 尝试获取锁失败后,进入阻塞状态,等待线程 1 释放锁,再去获取。

image-20220512010255785

死锁、活锁和饥饿可以用 java.util.concurrent 包下的 ReentrantLock 类解决,看下面介绍。

# 公平锁非公平锁

公平锁:是指多个线程按照申请锁的顺序来获取锁,类似排队打饭,先来后到。

非公平锁:是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比现申请的线程优先获取锁,在高并发的情况下,有可能会造成优先级反转或者饥饿现象。

// 无参
public ReentrantLock() {
	sync = new NonfairSync();
}
// 有参
public ReentrantLock(boolean fair) {
	sync = fair ? new FairSync() : new NonfairSync();
}
1
2
3
4
5
6
7
8

并发包中的 ReentrantLock 的创建可以指定构造函数的 boolean 类型来得到公平锁或者非公平锁,默认是非公平锁。

公平锁:就是很公平,在并发环境中,每个线程在获取到锁时会先查看此锁维护的等待队列,如果为空,或者当前线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照 FIFO(先进先出)的规则从队列中取到自己。

非公平锁:非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就会采用类似公平锁那种方式。

就 ReentrantLock 而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点 在于吞吐量比公平锁大。

对于 Synchronized 而言,也是一种非公平锁。

# 可重入锁

可重入锁(也叫递归锁),指的是同一线程外层函数获得锁之后,内层递归函数仍然能获取该锁的代码,在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁,也就是说,线程可以进入任何一个它已经拥有的锁,所同步着的代码块。好比家里进入大门之后,就可以进入里面的房间了。

ReentrantLock、Synchronized 就是一个典型的可重入锁。

可重入锁最大的作用就是避免死锁。

测试:Synchronized

public class SynchronizedDemo {
    public static void main(String[] args) {
        Phone phone = new Phone();
        // T1 线程在外层获取锁时,也会自动获取里面的锁
        new Thread(() -> {
            phone.sendSMS();
        }, "T1").start();

        new Thread(() -> {
            phone.sendSMS();
        }, "T2").start();
    }
}
class Phone {
    public synchronized void sendSMS() {
        System.out.println(Thread.currentThread().getName() + " sendSMS");
        sendEmail();
    }

    public synchronized void sendEmail() {
        System.out.println(Thread.currentThread().getName() + " sendEmail");
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 自旋锁

自旋锁(spinlock),是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗 CPU。

public class SpinLockDemo {
    // 原子引用线程, 没写参数,引用类型默认为 null
    AtomicReference<Thread> atomicReference = new AtomicReference<>();

    // 上锁
    public void myLock() {
        Thread thread = Thread.currentThread();
        System.out.println(Thread.currentThread().getName() + "==>mylock");
        // 自旋
        while (!atomicReference.compareAndSet(null, thread)) {
        }
    }

    //解锁
    public void myUnlock() {
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread, null);
        System.out.println(Thread.currentThread().getName() + "==>myUnlock");
    }

    public static void main(String[] args) throws InterruptedException {
        SpinLockDemo spinLockDemo = new SpinLockDemo();

        new Thread(() -> {
            spinLockDemo.myLock();
            try {
                TimeUnit.SECONDS.sleep(4);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            spinLockDemo.myUnlock();
        }, "T1").start();

        TimeUnit.SECONDS.sleep(1);

        new Thread(() -> {
            spinLockDemo.myLock();

            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            spinLockDemo.myUnlock();
        }, "T2").start();
    }

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

# ReentrantLock

相对于 synchronized 它具备如下特点:

  • 可中断
  • 可以设置超时时间
  • 可以设置为公平锁
  • 支持多个条件变量

与 synchronized 一样,都支持可重入

基本语法

// 获取锁
reentrantLock.lock();
try {
    // 临界区
} finally {
    // 释放锁
    reentrantLock.unlock();
}
1
2
3
4
5
6
7
8

# 可重入

可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次反复获取这把锁。

如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住。







 








 








 








public class Test {
    static ReentrantLock lock = new ReentrantLock();
    public static void main(String[] args) {
        method1();
    }
    public static void method1() {
        lock.lock();
        try {
            log.debug("execute method1");
            method2();
        } finally {
            lock.unlock();
        }
    }
    public static void method2() {
        lock.lock();
        try {
            log.debug("execute method2");
            method3();
        } finally {
            lock.unlock();
        }
    }
    public static void method3() {
        lock.lock();
        try {
            log.debug("execute method3");
        } finally {
            lock.unlock();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32

输出:

17:59:11.862 [main] c.TestReentrant - execute method1 
17:59:11.865 [main] c.TestReentrant - execute method2 
17:59:11.865 [main] c.TestReentrant - execute method3
1
2
3

此时是一个 main 线程,可以看到,第 7 行 main 线程获取了锁,16 行依然获取锁,然后 25 行也可以获取锁,代表可重入。

# 可打断

加锁后能被打断的 API 是 lockInterruptibly(),lockInterruptibly 方法和 lock 方法一样能获取锁,但又多出来一个特点:能被打断,即:

ReentrantLock lock = new ReentrantLock();
lock.lockInterruptibly();
1
2

示例:

public static void main(String[] args) {
    ReentrantLock lock = new ReentrantLock();
    Thread t1 = new Thread(() -> {
        log.debug("启动...");
        try {
            lock.lockInterruptibly();
        } catch (InterruptedException e) {
            e.printStackTrace();
            log.debug("等锁的过程中被打断");
            return;
        }
        try {
            log.debug("获得了锁");
        } finally {
            lock.unlock();
        }
    }, "t1");
    lock.lock();
    log.debug("获得了锁");
    t1.start();
    try {
        sleep(1);
        t1.interrupt();
        log.debug("执行打断");
    } finally {
        lock.unlock();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

输出:

18:02:40.520 [main] c.TestInterrupt - 获得了锁
18:02:40.524 [t1] c.TestInterrupt - 启动... 
18:02:41.530 [main] c.TestInterrupt - 执行打断
java.lang.InterruptedException 
 at 
java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchr
onizer.java:898) 
 at 
java.util.concurrent.locks.AbstractQueuedSynchronizer.acquireInterruptibly(AbstractQueuedSynchron
izer.java:1222) 
 at java.util.concurrent.locks.ReentrantLock.lockInterruptibly(ReentrantLock.java:335) 
 at cn.itcast.n4.reentrant.TestInterrupt.lambda$main$0(TestInterrupt.java:17) 
 at java.lang.Thread.run(Thread.java:748) 
18:02:41.532 [t1] c.TestInterrupt - 等锁的过程中被打断
1
2
3
4
5
6
7
8
9
10
11
12
13
14

注意如果是不可中断模式,那么即使使用了 interrupt 也不会让等待中断,如 lock 方法

public static void main(String[] args) {
    ReentrantLock lock = new ReentrantLock();
    Thread t1 = new Thread(() -> {
        log.debug("启动...");
        lock.lock();
        try {
            log.debug("获得了锁");
        } finally {
            lock.unlock();
        }
    }, "t1");
    lock.lock();
    log.debug("获得了锁");
    t1.start();
    try {
        sleep(1);
        t1.interrupt();
        log.debug("执行打断");
        sleep(1);
    } finally {
        log.debug("释放了锁");
        lock.unlock();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

输出:

18:06:56.261 [main] c.TestInterrupt - 获得了锁
18:06:56.265 [t1] c.TestInterrupt - 启动... 
18:06:57.266 [main] c.TestInterrupt - 执行打断 // 这时 t1 并没有被真正打断, 而是仍继续等待锁
18:06:58.267 [main] c.TestInterrupt - 释放了锁
18:06:58.267 [t1] c.TestInterrupt - 获得了锁
1
2
3
4
5

# 锁超时

线程尝试获取锁时,超出了一定时间,则放弃获取锁。

锁超时的 API 为 tryLock(),tryLock 方法会返回一个 boolean 值,如果获取到锁,则返回 true,如果获取不到锁,则返回 false。

即:

ReentrantLock lock = new ReentrantLock();
boolean isGetLock = lock.tryLock();
// 或者
boolean isGetLock = lock.tryLock(long timeout, TimeUnit unit); // TimeUnit 为时间单位,如秒、分、时,timeout 为时间值
1
2
3
4

锁超时又分为两者情况:

立刻失败

直接使用 lock.tryLock();,判断返回的 boolean 类型知道是否成功获取锁。

public static void main(String[] args) {
    ReentrantLock lock = new ReentrantLock();
    Thread t1 = new Thread(() -> {
        log.debug("启动...");
        if (!lock.tryLock()) {
            log.debug("获取立刻失败,返回");
            return;
        }
        try {
            log.debug("获得了锁");
        } finally {
            lock.unlock();
        }
    }, "t1");
    lock.lock();
    log.debug("获得了锁");
    t1.start();
    try {
        sleep(2);
    } finally {
        lock.unlock();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

输出:

18:15:02.918 [main] c.TestTimeout - 获得了锁
18:15:02.921 [t1] c.TestTimeout - 启动... 
18:15:02.921 [t1] c.TestTimeout - 获取立刻失败,返回
1
2
3

因为 main 线程先获取锁,2s 才释放锁,而 t1 线程直接尝试获取锁失败,返回 false。

超时失败

规定好超时时间,使用 lock.tryLock(long timeout, TimeUnit unit);,判断返回的 boolean 类型知道是否成功获取锁。

public static void main(String[] args) {
    ReentrantLock lock = new ReentrantLock();
    
    Thread t1 = new Thread(() -> {
        log.debug("启动...");
        try {
            if (!lock.tryLock(1, TimeUnit.SECONDS)) {
                log.debug("获取等待 1s 后失败,返回");
                return;
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        try {
            log.debug("获得了锁");
        } finally {
            lock.unlock();
        }
    }, "t1");
    lock.lock();
    log.debug("获得了锁");
    t1.start();
    try {
        sleep(2);
    } finally {
        lock.unlock();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

输出:

18:19:40.537 [main] c.TestTimeout - 获得了锁
18:19:40.544 [t1] c.TestTimeout - 启动... 
18:19:41.547 [t1] c.TestTimeout - 获取等待 1s 后失败,返回
1
2
3

main 线程先获取锁,然后睡眠 2s 才释放锁,而 t1 线程等待 1s 后无法获取锁,于是就返回 false。

# 使用 tryLock 解决哲学家就餐问题

因为筷子类是对象锁,所以直接令筷子类继承 ReentrantLock,这样筷子对象都拥有 ReentrantLock 的 API。

class Chopstick extends ReentrantLock {
    String name;
    public Chopstick(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "筷子{" + name + '}';
    }
}
1
2
3
4
5
6
7
8
9
10

哲学家类。当某个哲学家尝试获取左右筷子,如果获取失败,则放下自己手中的筷子给别人使用。

class Philosopher extends Thread {
    Chopstick left;
    Chopstick right;
    public Philosopher(String name, Chopstick left, Chopstick right) {
        super(name);
        this.left = left;
        this.right = right;
    }
    @Override
    public void run() {
        while (true) {
            // 尝试获得左手筷子
            if (left.tryLock()) {
                try {
                    // 尝试获得右手筷子
                    if (right.tryLock()) {
                        try {
                            eat();
                        } finally {
                            right.unlock();
                        }
                    }
                } finally {
                    left.unlock();
                }
            }
        }
    }
    private void eat() {
        log.debug("eating...");
        Sleeper.sleep(1);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

就餐测试

public class Test {
    public static void main(String[] args) {
        Chopstick c1 = new Chopstick("1");
        Chopstick c2 = new Chopstick("2");
        Chopstick c3 = new Chopstick("3");
        Chopstick c4 = new Chopstick("4");
        Chopstick c5 = new Chopstick("5");
        new Philosopher("苏格拉底", c1, c2).start();
        new Philosopher("柏拉图", c2, c3).start();
        new Philosopher("亚里士多德", c3, c4).start();
        new Philosopher("赫拉克利特", c4, c5).start();
        new Philosopher("阿基米德", c5, c1).start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 公平锁

公平锁是为了解决饥饿问题,即让所有线程都 平均 的获取锁。

如何开启公平锁呢?ReentrantLock 有个构造函数传入 boolean 值,就是是否开启公平锁。

ReentrantLock r = new ReentrantLock(boolean fair);
1

ReentrantLock 默认是不公平的,即 fair = false。

例子:

public static void main(String[] args) {
    ReentrantLock lock = new ReentrantLock(false);
    lock.lock();
    for (int i = 0; i < 500; i++) {
        new Thread(() -> {
            lock.lock();
            try {
                System.out.println(Thread.currentThread().getName() + " running...");
            } finally {
                lock.unlock();
            }
        }, "t" + i).start();
    }
    // 1s 之后去争抢锁
    Thread.sleep(1000);
    new Thread(() -> {
        System.out.println(Thread.currentThread().getName() + " start...");
        lock.lock();
        try {
            System.out.println(Thread.currentThread().getName() + " running...");
        } finally {
            lock.unlock();
        }
    }, "强行插入").start();
    lock.unlock();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26

强行插入,有机会在中间输出(注意:该实验不一定总能复现)。

t39 running... 
t40 running... 
t41 running... 
t42 running... 
t43 running... 
强行插入 start... 
强行插入 running... 
t44 running... 
t45 running... 
t46 running... 
t47 running... 
t49 running... 
1
2
3
4
5
6
7
8
9
10
11
12

改为公平锁后:

ReentrantLock lock = new ReentrantLock(true);
1

强行插入,总是在最后输出:

t465 running... 
t464 running... 
t477 running... 
t442 running... 
t468 running... 
t493 running... 
t482 running... 
t485 running... 
t481 running... 
强行插入 start...
强行插入 running...
1
2
3
4
5
6
7
8
9
10
11

公平锁一般没有必要,会降低并发度,后面分析原理时会讲解。

# 条件变量

synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时进入 waitSet 等待。

ReentrantLock 的条件变量比 synchronized 强大之处在于,它是支持多个条件变量的,这就好比:

  • synchronized 是那些不满足条件的线程都在一间休息室等消息
  • 而 ReentrantLock 支持多间休息室,有专门等烟的休息室、专门等早餐的休息室、唤醒时也是按休息室来唤醒

使用要点:

  • await 前需要获得锁
  • await 执行后,会释放锁,进入 conditionObject 等待
  • await 的线程被唤醒(或打断、或超时)取重新竞争 lock 锁
  • 竞争 lock 锁成功后,从 await 后继续执行

如何创建条件变量呢?利用 newCondition(),如下:

ReentrantLock lock = new ReentrantLock();
Condition c1 = lock.newCondition();
Condition c2 = lock.newCondition();

// c1 等待
c1.await();
// c2 等待
c2.await(long n);

// 唤醒 c1 的一个随机线程
c1.signal();  // 等价于 notify()
// 唤醒 c2 所有线程
c2.signalAll(); // 等价于 notifyAll()
1
2
3
4
5
6
7
8
9
10
11
12
13

在哪个线程里使用 c1.await(),则该线程暂停等待,如果需要唤醒,则在其他线程调用 c1.signal() 唤醒 c1 里的一个线程。

如果想唤醒 c1 的多个线程,则是 c1.signalAll()。

注意:c1 是一个空间,类似于休息室,在哪个线程调用 c1.await(),则该线程进入 c1 休息室,要想出来(唤醒),就等待其他线程调用 c1.signal() 或 c1.signalAll()。

例子:


 
 
 









 















 




















 










 






public static void main(String[] args) {
    static ReentrantLock lock = new ReentrantLock();
    static Condition waitCigaretteQueue = lock.newCondition();
    static Condition waitbreakfastQueue = lock.newCondition();
    static volatile boolean hasCigrette = false;
    static volatile boolean hasBreakfast = false;
    public static void main(String[] args) {
        new Thread(() -> {
            try {
                lock.lock();
                while (!hasCigrette) {
                    try {
                        // 该线程进入 waitCigaretteQueue 里等待
                        waitCigaretteQueue.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("等到了它的烟");
            } finally {
                lock.unlock();
            }
        }).start();
        new Thread(() -> {
            try {
                lock.lock();
                while (!hasBreakfast) {
                    try {
                        // 该线程进入 waitbreakfastQueue 里等待
                        waitbreakfastQueue.await();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                log.debug("等到了它的早餐");
            } finally {
                lock.unlock();
            }
        }).start();
        sleep(1);
        sendBreakfast();
        sleep(1);
        sendCigarette();
    }
    private static void sendCigarette() {
        lock.lock();
        try {
            log.debug("送烟来了");
            hasCigrette = true;
            // 唤醒 waitCigaretteQueue 里的一个随机线程
            waitCigaretteQueue.signal();
        } finally {
            lock.unlock();
        }
    }
    private static void sendBreakfast() {
        lock.lock();
        try {
            log.debug("送早餐来了");
            hasBreakfast = true;
            // 唤醒 waitbreakfastQueue 里的一个随机线程
            waitbreakfastQueue.signal();
        } finally {
            lock.unlock();
        }
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

输出:

18:52:27.680 [main] c.TestCondition - 送早餐来了
18:52:27.682 [Thread-1] c.TestCondition - 等到了它的早餐
18:52:28.683 [main] c.TestCondition - 送烟来了
18:52:28.683 [Thread-0] c.TestCondition - 等到了它的烟
1
2
3
4

# 区别

ReentrantLock 和 Synchronized 区别:

  • 首先 Synchronized 是 Java 内置关键字,在 JVM 层面,ReentrantLock 是个 Java 类
  • Synchronized 无法判断是否获取锁的状态,ReentrantLock 可以判断是否获取到锁
  • Synchronized 会自动释放锁(a 线程执行完同步代码会释放锁;b 线程执行过程中发生异常会释放锁),ReentrantLock 需在 finally 中手动释放锁(unlock() 方法释放锁),否则容易造成线程死锁
  • 用 Synchronized 关键字的两个线程 1 和线程 2,如果当前线程 1 获得锁,线程 2 线程等待。如果线程 1 阻塞,线程 2 则会一直等待下去,而 ReentrantLock 锁就不一定会等待下去,如果尝试获取不到锁,线程可以不用一直等待就结束了
  • Synchronized 的锁可重入、不可中断、非公平,而 ReentrantLock 锁可重入、可判断、可公平(两者皆可)
  • ReentrantLock 锁适合大量同步的代码的同步问题,Synchronized 锁适合代码少量的同步问题

# 同步模式之顺序控制

# 固定运行顺序

比如,必须先打印 2 后再打印 1。

# wait notify 版

创建一个对象锁和一个 boolean 对象,前者是线程锁,后者是判断先打印 1 还是先打印 2。

public static void main(String[] args) {
    // 用来同步的对象
    static Object obj = new Object();
    // t2 运行标记, 代表 t2 是否执行过
    static boolean t2runed = false;
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (obj) {
                // 如果 t2 没有执行过
                while (!t2runed) { 
                    try {
                        // t1 先等一会
                        obj.wait(); 
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
            System.out.println(1);
        });
        Thread t2 = new Thread(() -> {
            System.out.println(2);
            synchronized (obj) {
                // 修改运行标记
                t2runed = true;
                // 通知 obj 上等待的线程(可能有多个,因此需要用 notifyAll)
                obj.notifyAll();
            }
        });
        t1.start();
        t2.start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

# Park Unpark 版

可以看到,wait notify 版实现很麻烦:

  • 首先,需要保证先 wait 再 notify,否则 wait 线程永远得不到唤醒。因此使用了『运行标记』来判断该不该 wait
  • 第二,如果有些干扰线程错误的 notify 了 wait 线程,条件不满足时还要重新等待,使用了 while 循环来解决此问题
  • 最后,唤醒对象上的 wait 线程需要使用 notifyAll,因为『同步对象』上的等待线程可能不止一个

可以使用 LockSupport 类的 park 和 unpark 来简化上面的题目:

public static void main(String[] args) {
    Thread t1 = new Thread(() -> {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }
        // 当没有『许可』时,当前线程暂停运行;有『许可』时,用掉这个『许可』,当前线程恢复运行
        LockSupport.park();
        System.out.println("1");
    });

    Thread t2 = new Thread(() -> {
        System.out.println("2");
        // 给线程 t1 发放『许可』(多次连续调用 unpark 只会发放一个『许可』)
        LockSupport.unpark(t1);
    });
    t1.start();
    t2.start();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

park 和 unpark 方法比较灵活,他俩谁先调用,谁后调用无所谓。并且是以线程为单位进行『暂停』和『恢复』,不需要『同步对象』和『运行标记』。

# 交替输出

线程 1 输出 a 共 5 次,线程 2 输出 b 共 5 次,线程 3 输出 c 共 5 次。现在要求输出 abcabcabcabcabc 怎么实现?

# wait notify 版

利用 flag 来说存放当前线程。

写一个 print 方法,有三个参数:str 是打印的信息,curFlag 是当前线程,nextFlag 是下一个执行的线程。

当传过来的当前线程等于 flag,则打印该当前线程携带的信息,接着 curFlag 等于下一个执行的线程 nextFlag,然后唤醒所有线程,只有满足 nextFlag == falg 才打印携带的信息,其他线程虽然被唤醒,但是没有跳出 while 循环,继续 wait 等待。

class SyncWaitNotify {
    // 1 代表打印 a,2 代表打印 b,3 代表打印 c
    private int flag;
    // 循环次数
    private int loopNumber;
    
    public SyncWaitNotify(int flag, int loopNumber) {
        this.flag = flag;
        this.loopNumber = loopNumber;
    }
    // curFlag:当前线程,nextFlag:下一个执行的线程,str:打印的信息
    public void print(String str, int curFlag, int nextFlag) {
        for (int i = 0; i < loopNumber; i++) {
            synchronized (this) {
                while (this.flag != curFlag) {
                    try {
                        this.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.print(str);
                flag = nextFlag;
                this.notifyAll();
            }
        }
    }
}
public class Test {
    public static void main(String[] args) {
        // 初始化 1 打印,循环 5 次
        SyncWaitNotify syncWaitNotify = new SyncWaitNotify(1, 5);
        
        new Thread(() -> {
            syncWaitNotify.print(1, 2, "a");
        }).start();
        
        new Thread(() -> {
            syncWaitNotify.print(2, 3, "b");
        }).start();
        
        new Thread(() -> {
            syncWaitNotify.print(3, 1, "c");
        }).start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

# Lock 条件变量版

利用到了 ReentrantLock 的条件变量 Condition。

写一个 print 方法,有三个参数:str 是打印的信息,current 是当前条件变量,next 是下一个执行条件变量。

先让所有条件变量 current.wait() 等待,然后唤醒一个当前条件变量,该条件变量携带的信息打印后,唤醒下一个条件变量,打印下一个条件变量携带的信息,依次类推。

class AwaitSignal extends ReentrantLock {
    
    public void start(Condition first) {
        this.lock();
        try {
            log.debug("start");
            first.signal();
        } finally {
            this.unlock();
        }
    }
    
    public void print(String str, Condition current, Condition next) {
        for (int i = 0; i < loopNumber; i++) {
            this.lock();
            try {
                // 当前线程等待
                current.await();
                // 如果等待结束,则打印 str
                System.out.print(str);
                // 唤醒下一个线程
                next.signal();
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                this.unlock();
            }
        }
    }
    // 循环次数
    private int loopNumber;
    public AwaitSignal(int loopNumber) {
        this.loopNumber = loopNumber;
    }
}

public class Test {
    public static void main(String[] args) {
        AwaitSignal as = new AwaitSignal(5);
        Condition aWaitSet = as.newCondition();
        Condition bWaitSet = as.newCondition();
        Condition cWaitSet = as.newCondition();
        new Thread(() -> {
            as.print("a", aWaitSet, bWaitSet);
        }).start();
        
        new Thread(() -> {
            as.print("b", bWaitSet, cWaitSet);
        }).start();
        
        new Thread(() -> {
            as.print("c", cWaitSet, aWaitSet);
        }).start();
        // 初始化 aWaitSet 条件变量打印
        as.start(aWaitSet);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57

注意:该实现没有考虑 a,b,c 线程都就绪再开始。

# Park Unpark 版

class SyncPark {
    private int loopNumber;
    // 线程的集合数组
    private Thread[] threads;
    public SyncPark(int loopNumber) {
        this.loopNumber = loopNumber;
    }
    public void setThreads(Thread... threads) {
        this.threads = threads;
    }

    public void print(String str) {
        for (int i = 0; i < loopNumber; i++) {
            // 暂停当前线程
            LockSupport.park();
            // 暂停结束后打印信息
            System.out.print(str);
            // 唤醒下一个线程
            LockSupport.unpark(nextThread());
        }
    }

    private Thread nextThread() {
        // 获取当前线程
        Thread current = Thread.currentThread();
        int index = 0;
        for (int i = 0; i < threads.length; i++) {
            // 获取 threads 里的线程位置
            if(threads[i] == current) {
                index = i;
                break;
            }
        }
        // 返回下一个线程(当前线程位置 + 1)
        if(index < threads.length - 1) {
            return threads[index + 1];
        } else {
            return threads[0];
        }
    }

    public void start() {
        for (Thread thread : threads) {
            thread.start();
        }
        // 初始化d
        LockSupport.unpark(threads[0]);
    }
}
public class Test {
    public static void main(String[] args) {
        SyncPark syncPark = new SyncPark(5);

        Thread t1 = new Thread(() -> {
            syncPark.print("a");
        });

        Thread t2 = new Thread(() -> {
            syncPark.print("b");
        });

        Thread t3 = new Thread(() -> {
            syncPark.print("c\n");
        });

        syncPark.setThreads(t1, t2, t3);
        syncPark.start();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
编辑此页 (opens new window)
#JUC
更新时间: 2024/11/02, 18:07:00
JUC - 共享模式之锁基础
JUC - 共享模式之内存

← JUC - 共享模式之锁基础 JUC - 共享模式之内存→

最近更新
01
技术随笔 - Element Plus 修改包名 原创
11-02
02
Reactor - 扩展性
11-02
03
Reactor - 最佳实践
11-02
更多文章>
Theme by Vdoing | Copyright © 2021-2025 Young Kbt | blog
桂ICP备2021009994号
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式