当前位置: 首页 > news >正文

JUC并发编程:共享模型之管程与悲观锁(synchronized)详解

摘要:本文围绕 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 行代码会按顺序执行(无交错),结果准确:

  1. 读取静态变量 i 的值(0)
  2. 准备常数 1
  3. 执行加法运算(线程内 i=1)
  4. 将结果 1 写入静态变量 i
  5. 再次读取静态变量 i 的值(1)
  6. 准备常数 1
  7. 执行减法运算(线程内 i=0)
  8. 将结果 0 写入静态变量 i

最终静态变量 i 的结果为 0,运算正确。

多线程执行情况(可能出现交错)

多线程环境下,由于线程切换,8 行代码可能交错执行,导致结果异常:

情况 1:结果为负数

  1. 线程 2 读取静态变量 i 的值(0)
  2. 线程 2 准备常数 1
  3. 线程 2 执行减法运算(线程内 i=-1)
  4. 发生上下文切换
  5. 线程 1 读取静态变量 i 的值(0)
  6. 线程 1 准备常数 1
  7. 线程 1 执行加法运算(线程内 i=1)
  8. 线程 1 将结果 1 写入静态变量 i
  9. 发生上下文切换
  10. 线程 2 将结果 - 1 写入静态变量 i

最终静态变量 i 的结果为 - 1。

情况 2:结果为正数

  1. 线程 1 读取静态变量 i 的值(0)
  2. 线程 1 准备常数 1
  3. 线程 1 执行加法运算(线程内 i=1)
  4. 发生上下文切换
  5. 线程 2 读取静态变量 i 的值(0)
  6. 线程 2 准备常数 1
  7. 线程 2 执行减法运算(线程内 i=-1)
  8. 线程 2 将结果 - 1 写入静态变量 i
  9. 发生上下文切换
  10. 线程 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{@Overridepublic 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) {// 使用上述变量}}

     关于线程安全性的说明:

  1. map:不安全,HashMap不是线程安全的,多线程环境下操作会有并发问题
  2. S1:不安全,虽然String对象不可变,但S1引用可以被修改,多线程环境下可能不一致
  3. S2:安全,final保证引用不可变,且String本身不可变
  4. D1:不安全,Date是可变对象,多线程可修改其内部状态
  5. 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++操作不是原子操作,可能导致计数不准确。

要解决这个问题,可以:

  1. 使用synchronized关键字修饰update()方法
  2. 使用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) 的区别:

  1. sleep 是 Thread 方法,而 wait 是 Object 的方法

  2. sleep 不需要强制和 synchronized 配合使用,但 wait 需要 和 synchronized 一起用

  3. sleep 在睡眠的同时,不会释放对象锁,但 wait 在等待的时候会释放对象锁

  4. 它们执行后的状态均为 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 排查

    大功告成!

    http://www.dtcms.com/a/415007.html

    相关文章:

  • php基础-文件包含(第13天)
  • STM32智能加湿器
  • 网站开发管理nodejs网站开发教程
  • webrtc弱网-TrendlineEstimator类源码分析与算法原理
  • RocketMQ 消息堆积:快速定位、处理与预防方案
  • 深圳网站建设制作开发咨询邯郸网站建设
  • P3051题解
  • 想给孩子找点题做 都有什么网站化学课件
  • 【2026计算机毕业设计】基于Springboot的汉服交流的微信小程序
  • uutils coreutils - GNU coreutils 的 Rust 跨平台实现
  • 如何在阿里巴巴上做网站去哪网站备案吗
  • 软考中级-软件设计师(五)
  • 零基础学Docker(5)--容器数据卷
  • list列表
  • 团购网站做摄影网站编程开发
  • Kurt-Blender零基础教程:第4章:粒子篇
  • Qt常用控件之QTextEdit
  • ImageHash - Python 图像哈希库
  • 初识 Vue
  • 做网站销售水果上海建设安全协会网站
  • 正能量视频素材免费下载网站现代营销手段有哪些
  • Prj11-8088单板机C语言大综合(一)
  • 44.网络层
  • 肇庆网站制作软件郑州企业网络推广公司
  • ALLaM - 专为阿拉伯语设计的AI大语言模型
  • Docker Compose 停止命令对比
  • 北京网站推广优化更改wordpress端口
  • 优势的seo网站优化排名网站内容质量
  • Transformer 能做什么?—— 多领域应用全景
  • 认识RAG