摘要:本文围绕 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{

    @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) {
        // 使用上述变量
    }

}

     关于线程安全性的说明:

  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 排查

    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();
        }
    }

    大功告成!

    Logo

    有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

    更多推荐