【 JUC并发编程 | 共享模型之管程 】
摘要:本文围绕 Java 并发编程管程与悲观锁展开。先阐述共享模型下多线程操作共享变量的竞态条件及临界区概念;再讲 synchronized 使用、锁粒度优化,分析变量线程安全;接着介绍 wait/notify、Park/Unpark 及线程状态转换;最后探讨多锁优化并发与死锁等活跃性问题。


1. 共享模型之管程
1.1 共享带来的问题
1.1.1 Java 的体现
两个线程对初始值为 0 的静态变量一个做自增,一个做自减,各做 5000 次,结果是 0 吗?
static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter++;
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter--;
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}",counter);
}
问题分析
以上的结果可能是正数、负数、零。为什么呢?因为 Java 中对静态变量的自增,自减并不是原子操作,要彻底理解,必须从字节码来进行分析。
例如对于 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 行代码会按顺序执行(无交错),结果准确:

- 读取静态变量 i 的值(0)
- 准备常数 1
- 执行加法运算(线程内 i=1)
- 将结果 1 写入静态变量 i
- 再次读取静态变量 i 的值(1)
- 准备常数 1
- 执行减法运算(线程内 i=0)
- 将结果 0 写入静态变量 i
最终静态变量 i 的结果为 0,运算正确。
多线程执行情况(可能出现交错)
多线程环境下,由于线程切换,8 行代码可能交错执行,导致结果异常:

情况 1:结果为负数
- 线程 2 读取静态变量 i 的值(0)
- 线程 2 准备常数 1
- 线程 2 执行减法运算(线程内 i=-1)
- 发生上下文切换
- 线程 1 读取静态变量 i 的值(0)
- 线程 1 准备常数 1
- 线程 1 执行加法运算(线程内 i=1)
- 线程 1 将结果 1 写入静态变量 i
- 发生上下文切换
- 线程 2 将结果 - 1 写入静态变量 i
最终静态变量 i 的结果为 - 1。
情况 2:结果为正数

- 线程 1 读取静态变量 i 的值(0)
- 线程 1 准备常数 1
- 线程 1 执行加法运算(线程内 i=1)
- 发生上下文切换
- 线程 2 读取静态变量 i 的值(0)
- 线程 2 准备常数 1
- 线程 2 执行减法运算(线程内 i=-1)
- 线程 2 将结果 - 1 写入静态变量 i
- 发生上下文切换
- 线程 1 将结果 1 写入静态变量 i
最终静态变量 i 的结果为 1。
1.1.2 临界区
一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区
每个线程有自己的栈空间,但堆内存、静态变量、全局变量等是共享的。
如果多个线程只是读取共享资源,不会引发问题,因为读取操作不会改变资源状态。
关键点:
临界区是代码段,不是数据。
进入临界区的线程可能会修改共享资源。
若多个线程同时进入临界区,并且执行顺序不确定(指令交错),就可能导致数据不一致。
static int counter = 0;
static void increment()
// 临界区
{
counter++;
}
static void decrement()
// 临界区
{
counter--;
}
为什么是临界区?
counter是静态变量,属于共享资源。
increment()和decrement()中都对counter进行了写操作。如果多个线程同时调用这两个函数,就构成了对临界区的并发访问。
1.1.3 竞态条件
多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件。所以,竞态条件的本质是:程序的正确性依赖于线程执行的顺序,而这个顺序是不确定的。
1.2 synchronized 解决方案
应用之互斥
为了避免临界区的竞态条件发生,有多种手段可以达到目的。
-
阻塞式的解决方案:synchronized,Lock
-
非阻塞式的解决方案:原子变量
本次课使用阻塞式的解决方案:synchronized,来解决上述问题,即俗称的【对象锁】,它采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】,其它线程再想获取该【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换。
虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:
互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点
1.2.1 synchronized 语法
synchronized(同一个对象) // 线程1, 线程2(blocked)
{
临界区
}
使用synchronized解决问题
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 实际是用对象锁保证了临界区内代码的原子性,临界区内的代码对外是不可分割的,不会被线程切换所打断。
为了加深理解,请思考下面的问题:
1. 如果把 synchronized (obj) 放在 for 循环的外面,如何理解?-- 原子性
2. 如果 t1 synchronized (obj1) 而 t2 synchronized (obj2) 会怎样运作?-- 锁对象
3. 如果 t1 synchronized (obj) 而 t2 没有加会怎么样?如何理解?-- 锁对象
1.2.2 面向对象改进
把需要保护的共享变量放入一个类:
class Room {
int value = 0;
public void increment() {
synchronized (this) {
value++;
}
}
public void decrement() {
synchronized (this) {
value--;
}
}
public int get() {
synchronized (this) {
return value;
}
}
}
@Slf4j
public class Test1 {
public static void main(String[] args) throws InterruptedException {
Room room = new Room();
Thread t1 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
room.increment();
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int j = 0; j < 5000; j++) {
room.decrement();
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("count: {}" , room.get());
}
}
1.3 方法上的 synchronized

1.3.1 线程八锁
所谓的 "线程八锁",其实就是考察 synchronized 锁住的是哪个对象。
情况 1:12 或 21
@Slf4j(topic = "c.Number")
class Number{
public synchronized void a() {
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();
}
情况 2:1s 后 12,或 2 1s 后 1
@Slf4j(topic = "c.Number")
class Number{
public 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();
}
情况 3:3 1s 12 或 23 1s 1 或 32 1s 1
@Slf4j(topic = "c.Number")
class Number {
public synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
public void c() {
log.debug("3");
}
}
public class Main {
public static void main(String[] args) {
Number n1 = new Number();
new Thread(() -> { n1.a(); }).start();
new Thread(() -> { n1.b(); }).start();
new Thread(() -> { n1.c(); }).start();
}
}
情况 4:2 1s 后 1
@Slf4j(topic = "c.Number")
class Number {
public synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public class Main {
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(() -> { n1.a(); }).start();
new Thread(() -> { n2.b(); }).start();
}
}
锁的不是同一个对象(a 锁的n1,b 锁的n2)
情况 5:2 1s 后 1
@Slf4j(topic = "c.Number")
class Number {
// 静态同步方法 - 锁的是 Number.class 类对象
public static synchronized void a() {
sleep(1); // 睡眠1秒
log.debug("1");
}
// 实例同步方法 - 锁的是 this 对象(n1实例)
public synchronized void b() {
log.debug("2");
}
}
public class Main {
public static void main(String[] args) {
Number n1 = new Number();
new Thread(() -> { n1.a(); }).start();
new Thread(() -> { n1.b(); }).start();
}
}
静态同步方法 - 锁的是 Number.class 类对象
实例同步方法 - 锁的是 this 对象(n1实例)
情况 6:1s 后 12,或 2 1s 后 1
@Slf4j(topic = "c.Number")
class Number {
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public static synchronized void b() {
log.debug("2");
}
}
public class Main {
public static void main(String[] args) {
Number n1 = new Number();
new Thread(() -> { n1.a(); }).start();
new Thread(() -> { n1.b(); }).start();
}
}
情况 7:2 1s 后 1
@Slf4j(topic = "c.Number")
class Number {
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public synchronized void b() {
log.debug("2");
}
}
public class Main {
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(() -> { n1.a(); }).start();
new Thread(() -> { n2.b(); }).start();
}
}
情况 8:1s 后 12,或 2 1s 后 1
@Slf4j(topic = "c.Number")
class Number {
public static synchronized void a() {
sleep(1);
log.debug("1");
}
public static synchronized void b() {
log.debug("2");
}
}
public class Main {
public static void main(String[] args) {
Number n1 = new Number();
Number n2 = new Number();
new Thread(() -> { n1.a(); }).start();
new Thread(() -> { n2.b(); }).start();
}
}
1.4 变量的线程安全分析
成员变量和静态变量是否线程安全?
如果它们没有共享,则线程安全
如果它们被共享了,根据它们的状态是否能够改变,又分两种情况
-
如果只有读操作,则线程安全
-
如果有读写操作,则这段代码是临界区,需要考虑线程安全
局部变量是否线程安全?
局部变量是线程安全的
但局部变量引用的对象则未必
-
如果该对象没有逃离方法的作用访问,它是线程安全的
-
如果该对象逃离方法的作用范围,需要考虑线程安全
1.4.1 局部变量线程安全分析
public static void test1() {
int i = 10;
i++;
}
每个线程调用 test1 () 方法时局部变量 i,会在每个线程的栈帧内存中被创建多份,故不存在共享
public static void test1();
descriptor: ()V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=1, args_size=0
0: bipush 10
2: istore_0
3: iinc 0, 1
6: return
LineNumberTable:
line 10: 0
line 11: 3
line 12: 6
LocalVariableTable:
Start Length Slot Name Signature
3 4 0 i I
如图:

局部变量的引用稍有不同
先看一个成员变量的例子
class ThreadUnsafe {
ArrayList<String> list = new ArrayList<>();
public void method1(int loopNumber) {
for (int i = 0; i < loopNumber; i++) {
// 临界区, 会产生竞态条件
method2();
method3();
// 临界区结束
}
}
private void method2() {
list.add("1");
}
private void method3() {
list.remove(0);
}
}
执行结果其中一种情况是,如果线程 2 还未 add,线程 1 remove 就会报错
分析:
无论哪个线程中的 method2 引用的都是同一个对象中的 list 成员变量。
method3 与 method2 分析相同。

将 list 修改为局部变量
class ThreadSafe {
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
private void method2(ArrayList<String> list) {
list.add("1");
}
private void method3(ArrayList<String> list) {
list.remove(0);
}
}
无报错
分析:
list 是局部变量,每个线程调用时会创建其不同实例,没有共享。而 method2 的参数是从 method1 中传递过来的,与 method1 中引用同一个对象。method3 的参数分析与 method2 相同。

1.4.2 方法访问修饰符带来的思考
如果把 method2 和 method3 的方法修改为 public 会不会代理线程安全问题?
情况 1:有其它线程调用 method2 和 method3
情况 2:在 情况 1 的基础上,为 ThreadSafe 类添加子类,子类覆盖 method2 或 method3 方法
class ThreadSafe {
public final void method1(int loopNumber) {
ArrayList<String> list = new ArrayList<>();
for (int i = 0; i < loopNumber; i++) {
method2(list);
method3(list);
}
}
private void method2(ArrayList<String> list) {
list.add("1");
}
private void method3(ArrayList<String> list) {
list.remove(0);
}
}
class ThreadSafeSubClass extends ThreadSafe{
@Override
public void method3(ArrayList<String> list) {
new Thread(() -> {
list.remove(0);
}).start();
}
}
可以看出 private 或 final 提供【线程安全】的意义所在,请体会开闭原则中的【闭】
1.4.3 常见线程安全类
在 Java 中,以下类常被提及具备线程安全特性:
- String
- Integer
- StringBuffer
- Random
- Vector
- Hashtable
- java.util.concurrent 包下的各类(如 ConcurrentHashMap、LinkedBlockingQueue 等)
需要明确的是,这里所说的 “线程安全”,核心含义是指:当多个线程同时对这些类的同一个实例发起调用,执行其内部的某个公开方法时,该实例能够通过内部的同步机制(如 synchronized 关键字、CAS 操作等)保证数据操作的原子性、可见性和有序性,避免出现数据不一致、脏读、指令重排等线程安全问题,确保方法执行结果符合预期逻辑。
也可以理解为:
Hashtable table = new Hashtable();
new Thread(()->{
table.put("key", "value1");
}).start();
new Thread(()->{
table.put("key", "value2");
}).start();
它们的每个方法是原子的。
但注意它们多个方法的组合不是原子的,见后面分析。
1.4.4 线程安全类方法的组合

1.4.5 不可变类线程安全性
String、Integer 等都是不可变类,因为其内部的状态不可以改变,因此其方法都是线程安全的。
String 有 replace,substring 等方法【可以】改变值啊,那么这些方法是如何保证线程安全的呢?
public class Immutable{
private int value = 0;
public Immutable(int value){
this.value = value;
}
public int getValue(){
return this.value;
}
}
如果想增加一个增加的方法呢?
public class Immutable{
private final int value; // 使用final修饰,确保构造后无法修改
public Immutable(int value){
this.value = value;
}
public int getValue(){
return this.value;
}
public Immutable add(int v){
return new Immutable(this.value + v);
}
}
1.4.6 实例分析
例 1:
public class MyServlet extends HttpServlet {
// 是否安全? 不安全,多个线程共享一个HashMap实例,可能导致并发问题
Map<String,Object> map = new HashMap<>();
// 是否安全? 不安全,String虽然不可变,但如果有代码修改S1的引用会有线程安全问题
String S1 = "...";
// 是否安全? 安全,final修饰的String不可变,且引用不能被修改
final String S2 = "...";
// 是否安全? 不安全,Date是可变对象,多个线程可能同时修改其状态
Date D1 = new Date();
// 是否安全? 不安全,虽然引用不可变,但Date对象本身是可变的,可能被多个线程修改
final Date D2 = new Date();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
// 使用上述变量
}
}
关于线程安全性的说明:
map:不安全,HashMap不是线程安全的,多线程环境下操作会有并发问题S1:不安全,虽然String对象不可变,但S1引用可以被修改,多线程环境下可能不一致S2:安全,final保证引用不可变,且String本身不可变D1:不安全,Date是可变对象,多线程可修改其内部状态D2:不安全,final只保证引用不变,但Date对象本身可变,多线程仍可修改其内容
例 2:
public class MyServlet extends HttpServlet {
// 是否安全?
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 记录调用次数
private int count = 0;
public void update() {
// ...
count++;
}
}
存在线程安全问题。
UserServiceImpl中的count变量是实例变量,会被多个线程共享,当多个线程同时调用update()方法时,count++操作不是原子操作,可能导致计数不准确。要解决这个问题,可以:
- 使用
synchronized关键字修饰update()方法- 使用
AtomicInteger替代int类型的count
例 3:
@Aspect
@Component
public class MyAspect {
// 是否安全?
private long start = 0L;
@Before("execution(* *(..))")
public void before() {
start = System.nanoTime();
}
@After("execution(* *(..))")
public void after() {
long end = System.nanoTime();
System.out.println("cost time:" + (end-start));
}
}
这个切面类存在线程安全问题。
start是实例变量,会被多个线程共享。当多个线程同时执行被切面拦截的方法时,可能出现一个线程的before()方法覆盖了另一个线程设置的start值,导致计算的时间不准确。静态变量定被所有线程共享;实例变量是否被共享,取决于它所属对象是否被多个线程共享在 Servlet 场景中,正因为 Servlet 实例被所有请求线程共享,所以它的实例变量才会被多线程共享。
例 4:
public class MyServlet extends HttpServlet {
// 是否安全
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 是否安全
private UserDao userDao = new UserDaoImpl();
public void update() {
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
public void update() {
String sql = "update user set password = ? where username = ?";
// 是否安全
try (Connection conn = DriverManager.getConnection("","","")){
// ...
} catch (Exception e) {
// ...
}
}
}
1. userService:本身的引用是安全的,但安全性取决于UserServiceImpl的实现
2. userDao:在UserServiceImpl中作为实例变量,其安全性取决于UserDaoImpl的实现
3. Connection:在update()方法内部创建的局部变量,每个线程调用时都会创建新的连接对象,是线程安全的
例 5:
public class MyServlet extends HttpServlet {
// 是否安全
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
// 是否安全
private UserDao userDao = new UserDaoImpl();
public void update() {
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
// 是否安全
private Connection conn = null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("","","");
// ...
conn.close();
}
}
UserDaoImpl中的conn变量存在严重的线程安全问题:
conn是实例变量,会被多个线程共享- 当多个线程同时调用
update()方法时,可能出现一个线程关闭了另一个线程正在使用的连接- 可能导致一个线程的
conn被另一个线程覆盖,造成连接泄露或操作错误
例 6:
public class MyServlet extends HttpServlet {
// 是否安全
private UserService userService = new UserServiceImpl();
public void doGet(HttpServletRequest request, HttpServletResponse response) {
userService.update(...);
}
}
public class UserServiceImpl implements UserService {
public void update() {
UserDao userDao = new UserDaoImpl();
userDao.update();
}
}
public class UserDaoImpl implements UserDao {
// 是否安全
private Connection conn = null;
public void update() throws SQLException {
String sql = "update user set password = ? where username = ?";
conn = DriverManager.getConnection("","","");
// ...
conn.close();
}
}
1.5 wait notify
1.5.1 API 介绍及原理
-
obj.wait () 让进入 object 监视器的线程到 waitSet 等待
-
obj.notify () 在 object 上正在 waitSet 等待的线程中挑一个唤醒
-
obj.notifyAll () 让 object 上正在 waitSet 等待的线程全部唤醒


这三个方法是线程间进行协作通信的重要手段,均属于 Object 类的原生方法。需要注意的是,调用这些方法的前提是当前线程必须已经获取到该对象的监视器锁(即处于 synchronized 同步块或同步方法中),否则会抛出 IllegalMonitorStateException 异常。
1.5.2 wait notify 的正确姿势
开始之前先看看 sleep (long n) 和 wait (long n) 的区别:
-
sleep 是 Thread 方法,而 wait 是 Object 的方法
-
sleep 不需要强制和 synchronized 配合使用,但 wait 需要 和 synchronized 一起用
-
sleep 在睡眠的同时,不会释放对象锁,但 wait 在等待的时候会释放对象锁
-
它们执行后的状态均为 TIMED_WAITING
方法一:
static final Object room = new Object();
static boolean hasCigarette = false;
static boolean hasTakeout = false;
思考下面的解决方案好不好,为什么?
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
sleep(2);
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
}
}
}, "小南").start();
for (int i = 0; i < 5; i++) {
new Thread(() -> {
synchronized (room) {
log.debug("可以开始干活了");
}
}, "其它人").start();
}
sleep(1);
new Thread(() -> {
// 这里需要加 synchronized (room)
synchronized (room) {
hasCigarette = true;
log.debug("烟到了噢!");
}
}, "送烟的").start();
1. 其他干活线程:阻塞效率低(门被小南反锁)
小南线程进入
synchronized (room)后,会持有 room 锁 2 秒(通过Thread.sleep(2000)实现)。这期间 5 个 "其他人" 线程会一直阻塞在room锁的等待队列中 —— 相当于小南在房间里反锁了门睡觉,其他人想进门干活只能在门外排队,完全无法并行,效率极低。2. 小南线程:烟提前到也无法唤醒(死等 2 秒,不 "听通知")
小南判断 "没烟" 后,用
Thread.sleep(2000)强制等待 2 秒,而非room.wait():
sleep()不会释放锁,即使 1 秒后送烟线程把烟送到,小南也不会立刻醒来,必须硬等够 2 秒才能再次判断 "是否有烟";- 反观
wait(),会主动释放锁并进入等待状态,一旦收到notify()通知(烟送到)就能立刻唤醒,无需无效等待。
解决方法,使用 wait - notify 机制
方法二:
// 新增共享条件:是否有外卖(模拟多个等待条件)
static boolean hasTakeout = false;
static final Object room = new Object();
static boolean hasCigarette = false;
public static void main(String[] args) throws InterruptedException {
// 小南:等烟
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait(2000); // 等待烟的条件
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 被唤醒后重新判断条件(避免虚假唤醒)
if (hasCigarette) {
log.debug("有烟了,可以开始干活了");
} else {
log.debug("等了2秒还没烟,放弃干活");
}
}
}, "小南").start();
// 小北:等外卖(新增的等待其他条件的线程)
new Thread(() -> {
synchronized (room) {
log.debug("有外卖没?[{}]", hasTakeout);
if (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
room.wait(2000); // 等待外卖的条件
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 被唤醒后重新判断条件
if (hasTakeout) {
log.debug("有外卖了,可以开始吃饭了");
} else {
log.debug("等了2秒还没外卖,放弃吃饭");
}
}
}, "小北").start();
// 其他干活线程
for (int i = 0; i < 3; i++) {
new Thread(() -> {
synchronized (room) {
log.debug("可以开始干活了");
}
}, "其它人-" + i).start();
}
// 1秒后先送烟
Thread.sleep(1000);
new Thread(() -> {
synchronized (room) {
hasCigarette = true;
log.debug("烟到了噢!");
room.notify(); // 随机唤醒一个等待线程(可能是小南,也可能是小北)
}
}, "送烟的").start();
// 再等1秒送外卖
Thread.sleep(1000);
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖到了噢!");
room.notify(); // 唤醒剩下的等待线程
}
}, "送外卖的").start();
}
当有多个线程等待不同条件(如小南等烟、小北等外卖),原代码用
room.notify()会有问题:
notify()会随机唤醒一个在room上等待的线程,无法精准唤醒对应条件的线程;- 例如:送烟线程调用
notify(),可能误唤醒等待外卖的小北 —— 小北被唤醒后检查条件(hasTakeout仍为false),发现不是自己等的条件,最终只能放弃,导致 "唤醒无效";- 同时,真正需要唤醒的小南可能仍在等待,直到
wait(2000)超时才醒来,造成逻辑混乱。
方法三:多个线程等待不同条件解决方案
// 共享变量
static boolean hasTakeout = false;
static final Object room = new Object();
static boolean hasCigarette = false;
public static void main(String[] args) throws InterruptedException {
// 小南:等烟(条件1)
new Thread(() -> {
synchronized (room) {
log.debug("有烟没?[{}]", hasCigarette);
if (!hasCigarette) {
log.debug("没烟,先歇会!");
try {
room.wait(); // 无超时等待(永久等,直到被唤醒)
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("有烟没?[{}]", hasCigarette);
if (hasCigarette) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小南").start();
// 小女:等外卖(条件2)
new Thread(() -> {
synchronized (room) {
log.debug("外卖送到没?[{}]", hasTakeout);
if (!hasTakeout) {
log.debug("没外卖,先歇会!");
try {
room.wait(); // 无超时等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("外卖送到没?[{}]", hasTakeout);
if (hasTakeout) {
log.debug("可以开始干活了");
} else {
log.debug("没干成活...");
}
}
}, "小女").start();
// 1秒后只送外卖(仅1个唤醒源)
Thread.sleep(1);
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖到了噢!");
room.notify(); // 随机唤醒1个等待线程
}
}, "送外卖的").start();
}
notify 只能随机唤醒一个 WaitSet 中的线程,这时如果有其它线程也在等待,那么就可能唤醒不了正确的线程,称之为【虚假唤醒】。
方法四:notifyAll
new Thread(() -> {
synchronized (room) {
hasTakeout = true;
log.debug("外卖到了噢!");
room.notifyAll();
}
}, "送外卖的").start();
notifyAll()虽然能唤醒所有等待线程,避免 "漏唤醒",但如果用if + wait()判断条件,线程被唤醒后仅会检查一次条件—— 若此时条件仍不成立(比如被 “虚假唤醒”),线程就会直接退出等待逻辑,失去重新等待的机会,最终导致业务逻辑异常。
方法四:while + wait
synchronized (room) {
while (!hasCigarette) { // 初始条件不成立,进入循环
log.debug("没烟,先歇会!");
room.wait(); // 被外卖唤醒后,回到while头部重新判断
// 重新判断:hasCigarette仍为false,继续循环
}
// 只有当hasCigarette为true时,才会走到这里
log.debug("有烟没?[{}]", hasCigarette); // 输出:有烟没?[true]
log.debug("可以开始干活了"); // 正确执行
}
1.7 Park & Unpark
1.7.1 基本使用
它们是 LockSupport 类中的方法:
// 暂停当前线程
LockSupport.park();
// 恢复某个线程的运行
LockSupport.unpark(暂停线程对象)
先 park 再 unpark
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);
先 unpark 再 park
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.7.2 特点
LockSupport.park/unpark 与 Object.wait/notify 作为 Java 线程阻塞 / 唤醒的两种核心机制,核心差异可总结为以下三点:
锁依赖不同
wait/notify/notifyAll必须在synchronized同步块中使用,强依赖于对象的 Monitor 锁;park/unpark无需依赖任何锁依赖,可直接通过线程实例操作阻塞 / 唤醒,无需关联同步锁。
唤醒精度不同
notify随机唤醒一个在锁上等待的线程,notifyAll唤醒所有等待线程,均无法精准指定目标线程,存在 “虚假唤醒” 或 “无效唤醒” 问题;park/unpark以线程为单位精准控制,unpark(Thread t)可直接唤醒指定线程,无随机唤醒风险。
超前操作有效性不同
wait/notify中,若notify先于wait执行(无线程等待),唤醒信号 "失效",后续wait仍阻塞;park/unpark支持 "超前唤醒",先调用unpark颁发 "许可",后续park直接消耗许可,不阻塞。park/unpark:先喝解药再喝毒药,就不会中毒
简言之,wait/notify 适合基于锁的条件等待场景,而 park/unpark 更适合线程级的精准控制,是并发工具的底层实现基础。
1.8 重新理解线程状态转换

情况 1 NEW → RUNNABLE
当线程对象调用 t.start() 方法时,线程状态由 NEW → RUNNABLE。此时线程进入可运行状态,等待操作系统调度获取 CPU 时间片。
情况 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();
private static final Logger log = LoggerFactory.getLogger(TestWaitNotify.class);
public static void main(String[] args) throws InterruptedException {
// 线程t1:获取锁后等待
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 释放锁,进入WAITING状态
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码...."); // 被唤醒并获取锁后执行
}
}, "t1").start();
// 线程t2:获取锁后等待
new Thread(() -> {
synchronized (obj) {
log.debug("执行....");
try {
obj.wait(); // 释放锁,进入WAITING状态
} catch (InterruptedException e) {
e.printStackTrace();
}
log.debug("其它代码...."); // 被唤醒并获取锁后执行
}
}, "t2").start();
// 主线程休眠0.5秒,确保t1、t2进入等待状态
Thread.sleep(500);
log.debug("唤醒 obj 上其它线程");
// 主线程获取锁后唤醒所有等待线程
synchronized (obj) {
obj.notifyAll(); // 唤醒obj对象等待队列中所有线程
}
}
}
情况 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
1.9 多把锁
1.9.1 多把不相干的锁
一间大屋子包含两个互不相干的功能:睡觉和学习。
若小南想学习,小女想睡觉时,若仅用一个屋子(对应一个对象锁),会导致两人无法同时进行活动,并发效率很低。
优化方案是:为两个功能分别准备独立的房间(对应多个对象锁)。这样一来,学习和睡觉可以并行进行,互不干扰,有效提升了并发度。
例如:
class BigRoom {
public void sleep() {
synchronized (this) {
log.debug("sleeping 2 小时");
Sleeper.sleep(2);
}
}
public void study() {
synchronized (this) {
log.debug("study 1 小时");
Sleeper.sleep(1);
}
}
}
改进
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);
}
}
}
好处
能显著增强程序的并发度。原本多个互不相干的功能(如睡觉、学习)因共用一把锁而互相阻塞,细分锁后,每个功能仅占用自身对应的专用锁,不同功能的线程可同时执行,互不干扰,大幅减少了不必要的等待时间,提升了整体执行效率。
坏处
会增加死锁的风险。若某个线程需要同时完成多个关联操作(例如 “先到书房拿书,再到卧室休息”),就可能需要依次获取多把锁(学习用的锁、睡觉用的锁)。一旦多个线程获取锁的顺序不一致(比如线程 A 先拿学习锁再等睡觉锁,线程 B 先拿睡觉锁再等学习锁),就会陷入互相等待对方释放锁的僵局,导致死锁。
1.10 活跃性
1.10.1 死锁
有这样的情况:一个线程需要同时获取多把锁,这时就容易发生死锁。
t1 线程 获得 A 对象 锁,接下来想获取 B 对象的锁。
t2 线程 获得 B 对象 锁,接下来想获取 A 对象的锁。
例:
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();
1.10.2 定位死锁
检测死锁可以使用 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
cmd > jstack 33200
Picked up JAVA_TOOL_OPTIONS: -Dfile.encoding=UTF-8
2018-12-29 05:51:40
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.91-b14 mixed mode):
"DestroyJavaVM" #13 prio=5 os_prio=0 tid=0x000000003525000 nid=0x2f60 waiting on condition [0x000000000000000]
java.lang.Thread.State: RUNNABLE
"Thread-1" #12 prio=5 os_prio=0 tid=0x00000001eb69000 nid=0xd40 waiting for monitor entry [0x00000001f54f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at thread.TestDeadLock.lambda$main$1(TestDeadLock.java:28)
- waiting to lock <0x000000076b5bf1c0> (a java.lang.Object)
- locked <0x000000076b5bf1d0> (a java.lang.Object)
at thread.TestDeadLock$$Lambda$2/883049899.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
"Thread-0" #11 prio=5 os_prio=0 tid=0x00000001eb68800 nid=0x1b28 waiting for monitor entry [0x00000001f44f000]
java.lang.Thread.State: BLOCKED (on object monitor)
at thread.TestDeadLock.lambda$main$0(TestDeadLock.java:15)
- waiting to lock <0x000000076b5bf1d0> (a java.lang.Object)
- locked <0x000000076b5bf1c0> (a java.lang.Object)
at thread.TestDeadLock$$Lambda$1/495053715.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
// 略去部分输出
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00000000361d378 (object 0x000000076b5bf1c0, a java.lang.Object),
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x00000000361e768 (object 0x000000076b5bf1d0, a java.lang.Object),
which is held by "Thread-1"
Java stack information for the threads listed above:
===================================================
"Thread-1":
at thread.TestDeadLock.lambda$main$1(TestDeadLock.java:28)
- waiting to lock <0x000000076b5bf1c0> (a java.lang.Object)
- locked <0x000000076b5bf1d0> (a java.lang.Object)
at thread.TestDeadLock$$Lambda$2/883049899.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
"Thread-0":
at thread.TestDeadLock.lambda$main$0(TestDeadLock.java:15)
- waiting to lock <0x000000076b5bf1d0> (a java.lang.Object)
- locked <0x000000076b5bf1c0> (a java.lang.Object)
at thread.TestDeadLock$$Lambda$1/495053715.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
Found 1 deadlock.
无代码,提取文字内容为:
- 避免死锁要注意加锁顺序
- 另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到 CPU 占用高的 Java 进程,再利用
top -Hp 进程id来定位是哪个线程,最后再用 jstack 排查
1.10.3 哲学家就餐问题

有五位哲学家,围坐在圆桌旁。
他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。
如果筷子被身边的人拿着,自己就得等待
筷子类
class Chopstick {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "筷子{" + name + '}';
}
}
哲学家类
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();
// 放下右手筷子
}
// 放下左手筷子
}
}
}
}
就餐
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();
执行不多会,就执行不下去了
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.10.4 活锁
活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束。
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();
}
// 假设有 sleep 方法,例如:
private static void sleep(double seconds) {
try {
Thread.sleep((long) (seconds * 1000));
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
1.10.5 饥饿
很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束,饥饿的情况不易演示,讲读写锁时会涉及饥饿问题
下面我讲一下我遇到的一个线程饥饿的例子,先来看看使用顺序加锁的方式解决之前的死锁问题

顺序加锁的解决方案

1.11 ReentrantLock
ReentrantLock 是 Java 并发包concurrent.locks 中的一个锁实现,相对于内置的 synchronized 关键字,它提供了更灵活、更强大的锁机制,主要具备如下特点:
可中断
-
补充说明:当线程尝试获取锁但被阻塞时,如果使用
lockInterruptibly()方法,它可以响应来自其他线程的interrupt()中断请求,从而提前结束等待状态并抛出InterruptedException。这有助于避免线程出现不可控的长时间阻塞,提高了程序对中断的响应能力。 -
对比:
synchronized在等待锁的过程中是无法被中断的,线程只能一直阻塞直到获取到锁。
可以设置超时时间
-
补充说明:通过
tryLock(long timeout, TimeUnit unit)方法,线程可以尝试在指定的时间内获取锁。如果在超时时间内成功获取则返回true,超时后仍未获取则返回false。这个特性可以有效避免死锁,因为线程不会无休止地等待下去,从而提供了更多的故障恢复手段。 -
对比:
synchronized在获取锁失败时会无限期等待,没有超时机制。
可以设置为公平锁
-
补充说明:在创建
ReentrantLock时,可以通过构造函数传入true来将其指定为公平锁。公平锁会按照线程请求锁的先后顺序来分配锁,即 "先到先得",这保证了等待时间最长的线程会优先获得锁,从而避免了线程饥饿现象。 -
对比:
synchronized内置的锁是非公平的,不保证等待队列中的线程获取锁的顺序。而ReentrantLock默认也是非公平的,但它提供了可选的公平模式,增加了灵活性。
支持多个条件变量
-
补充说明:一个
ReentrantLock对象可以关联多个Condition(条件队列)实例。这意味着线程可以在不同的条件上等待和唤醒,从而实现更精细的线程间通信。例如,在生产者-消费者模型中,可以为"非满" 和 "非空"条件分别创建Condition,从而更精确地通知等待的生产者或消费者线程。 -
对比:
synchronized锁对象只关联一个隐式的等待集合(通过wait(),notify(),notifyAll()操作),所有等待的线程都在同一个队列里,无法区分等待的条件,可能导致不必要的唤醒(惊群效应)。
与 synchronized 一样,都支持可重入
-
补充说明:可重入性意味着同一个线程在已经持有锁的情况下,可以再次获取该锁而不会造成死锁。这对于递归调用或需要多次进入同步代码块的场景至关重要。
ReentrantLock和synchronized都具备这一核心特性,确保了锁行为的正确性和便利性。
1.11.1 基本语法
// 获取锁
reentrantLock.lock();
try {
// 临界区
} finally {
// 释放锁
reentrantLock.unlock();
}
1.11.2 可重入
可重入是指同一个线程如果首次获得了这把锁,那么因为它是这把锁的拥有者,因此有权利再次获取这把锁
如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住
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.11.3 可打断
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.11.4 锁超时
立刻失败
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();
}
超时失败
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();
return;
}
try {
log.debug("获得了锁");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
log.debug("获得了锁");
t1.start();
try {
sleep(2);
} finally {
lock.unlock();
}
1.11.5 使用 tryLock 解决问题
class Chopstick extends ReentrantLock {
String name;
public Chopstick(String name) {
this.name = name;
}
@Override
public String toString() {
return "筷子{" + name + '}';
}
}
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.11.6 公平锁
ReentrantLock 默认是不公平的
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();
强行插入,有机会在中间输出
改为公平锁后
ReentrantLock lock = new ReentrantLock(true);
强行插入,总是在最后输出
公平锁一般没有必要,会降低并发度,后面分析原理时会讲解
1.11.7 条件变量
synchronized 中也有条件变量,就是我们讲原理时那个 waitSet 休息室,当条件不满足时线程进入 waitSet 等待。
ReentrantLock 的条件变量比 synchronized 强大之处在于,它支持多个条件变量(Condition)
synchronized:所有不满足条件的线程都在同一间休息室中等消息,无论它们等待的条件是什么。
ReentrantLock:支持多间休息室,比如有专门等烟的休息室、专门等早餐的休息室,唤醒时也是按休息室来分别唤醒,实现了更精细的线程调度。
使用要点
1. await 前需要获得锁:在调用 condition.await() 之前,线程必须已经持有与该条件变量关联的 ReentrantLock 锁。
2. await 执行后,会释放锁,进入 conditionObject 等待:当线程执行 await() 时,会释放当前持有的锁,并进入该条件变量对应的等待队列中。
3. await 的线程被唤醒(或打断、或超时)后需重新竞争 lock 锁:当其他线程调用 signal() 或 signalAll() 唤醒等待线程时,被唤醒的线程会从等待队列移出,但需重新参与锁竞争。
4. 竞争 lock 锁成功后,从 await 后继续执行:一旦被唤醒的线程成功再次获取到锁,它将从 await() 方法返回,并继续执行后续代码。
例子
static ReentrantLock lock = new ReentrantLock();
static Condition waitCigaretteQueue = lock.newCondition();
static Condition waitBreakfastQueue = lock.newCondition();
static volatile boolean hasCigarette = false;
static volatile boolean hasBreakfast = false;
public static void main(String[] args) {
new Thread(() -> {
try {
lock.lock();
while (!hasCigarette) {
try {
waitCigaretteQueue.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
log.debug("等到了它的烟");
} finally {
lock.unlock();
}
}).start();
new Thread(() -> {
try {
lock.lock();
while (!hasBreakfast) {
try {
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("送烟来了");
hasCigarette = true;
waitCigaretteQueue.signal();
} finally {
lock.unlock();
}
}
private static void sendBreakfast() {
lock.lock();
try {
log.debug("送早餐来了");
hasBreakfast = true;
waitBreakfastQueue.signal();
} finally {
lock.unlock();
}
}
大功告成!
更多推荐
所有评论(0)