Java并发包中的管程:Lock和Condition

发布于:2025-06-07 ⋅ 阅读:(16) ⋅ 点赞:(0)

1. Java并发包中的管程:Lock和Condition

1.1. Lock 的核心意义:再造管程

Java 原生的 synchronized 关键字已经是管程的一种实现,那为什么还要提供 Lock 接口?

原因是:

Lock 弥补了 synchronized 的三大缺陷,尤其是在解决“死锁”中的不可抢占条件时具有更强的灵活性。

三种解决“不可抢占”的能力(也是 Lock 的优势):

1. 可中断获取锁

void lockInterruptibly() throws InterruptedException;
  • 遇到死锁时可通过中断来释放已有资源。

2. 支持超时的获取锁

boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
  • 限时等待,避免无限期阻塞。

3. 非阻塞地获取锁

boolean tryLock();
  • 获取不到立即返回,避免线程挂起。

1.2. Lock 的可见性保障原理

问题:

在使用 Lock 的并发场景中,如何确保对共享变量修改的可见性?

解决方案:

基于 Java 内存模型的 Happens-Before 原则ReentrantLock 内部的 volatile state 实现:

  • 解锁前后的顺序性保证(线程内)
  • volatile 变量规则(state 的读写)
  • 传递性规则:T1 修改变量 → 解锁 → T2 加锁 → 可见

1.3. 可重入锁(ReentrantLock)、公平锁与非公平锁

1. 可重入锁(ReentrantLock)

可重入锁的定义:指的是线程可以重复获取同一把锁。

示例:

例如下面代码中,当线程 T1 执行到 ① 处时,已经获取到了锁 rtl ,当在 ① 处调用 get() 方法时,会在 ② 再次对锁 rtl 执行加锁操作。此时,如果锁 rtl 是可重入的,那么线程 T1 可以再次加锁成功;如果锁 rtl 是不可重入的,那么线程 T1 此时会被阻塞。

class X {
    private final Lock rtl =
    new ReentrantLock();
    int value;
    public int get() {
        // 获取锁
        rtl.lock();         ②
        try {
            return value;
        } finally {
            // 保证锁能释放
            rtl.unlock();
        }
    }
    public void addOne() {
        // 获取锁
        rtl.lock();  
        try {
            value = 1 + get(); ①
        } finally {
            // 保证锁能释放
            rtl.unlock();
        }
    }
}
  • 可重入锁可避免同一个线程因为重复获取锁而造成死锁。

可重入函数(线程安全的):

重入函数,指的是多个线程可以同时调用该函数,每个线程都能得到正确结果;同时在一个线程内支持线程切换,无论被切换多少次,结果都是正确的。

2. 公平锁与非公平锁

区别:

  • 公平锁:先到先得,排队唤醒,避免线程“饿死”
  • 非公平锁(默认):允许插队,有更好的性能表现
// 无参构造函数:默认非公平锁
public ReentrantLock() {
    sync = new NonfairSync();
}
// 根据公平策略参数创建锁
public ReentrantLock(boolean fair){
    sync = fair ? new FairSync() 
                : new NonfairSync();
}

1.4. 用锁的最佳实践

最佳实践(出自 Doug Lea):

  1. 只在更新对象的成员变量时加锁
  2. 只在访问可变成员变量时加锁
  3. 不要在加锁后调用其他对象的方法
    • 可能造成不可控延迟(例如 IO、sleep)
    • 可能加其他锁,引发死锁风险

补充建议:

  • 减少锁的持有时间(快速释放)
  • 降低锁的粒度(尽可能小范围加锁)
  • 避免锁的嵌套调用

总结:

  • Lock 提供了比 synchronized 更丰富和灵活的控制机制。
  • 使用 Lock 编写并发程序时,应牢记:
    • 能不能中断?
    • 能不能超时?
    • 能不能快速失败?
  • 最好的并发代码是简单、清晰、易于分析的。

1.5. Condition的概念

Lock 与 synchronized 的区别:

  • 支持中断响应(lock.lockInterruptibly()
  • 支持超时获取锁(tryLock(timeout)
  • 支持非阻塞获取锁(tryLock()

Condition 是什么?

  • 管程中的条件变量
  • Object.wait/notify 相比,Condition 支持多个条件变量,适用于更复杂的并发场景(如阻塞队列:notEmptynotFull)。

Condition 使用示例:阻塞队列

public class BlockedQueue<T> {
  final Lock lock = new ReentrantLock();
  final Condition notFull = lock.newCondition();   // 队列不满
  final Condition notEmpty = lock.newCondition();  // 队列不空

  void enq(T x) {
    lock.lock();
    try {
      while (队列已满) {
        notFull.await();
      }
      // 执行入队操作...
      notEmpty.signal();  // 通知可以出队
    } finally {
      lock.unlock();
    }
  }

  void deq() {
    lock.lock();
    try {
      while (队列为空) {
        notEmpty.await();
      }
      // 执行出队操作...
      notFull.signal();  // 通知可以入队
    } finally {
      lock.unlock();
    }
  }
}
  • 注意:Lock 和 Condition 实现的管程,线程等待和通知需要调用 await()、signal()、signalAll(),它们的语义和 wait()、notify()、notifyAll() 是相同的。

1.6. 异步与同步的本质

同步: 调用方必须等待结果返回(如函数调用阻塞当前线程)。

异步: 调用方无需等待结果,调用立即返回,后续结果通过回调或其他方式通知。

举例:

pai1M(); // 如果阻塞等结果 -> 同步
printf("hello world"); // 若立即执行,不等待上面 -> 异步

实现异步的两种方式:

  1. 调用方创建线程: 在子线程中调用(常见于主线程不被阻塞的情况)。
  2. 被调用方创建线程: 方法中主动异步处理逻辑,主线程立即 return。

1.7. 异步转同步

异步转同步:

调用本来是异步执行的,但我们人为“阻塞住”当前线程,等它的结果返回后再继续执行,从而模拟出同步调用的效果。

知识点

说明

异步调用

不等待结果,直接返回

同步调用

等待结果,阻塞线程

Dubbo 的异步转同步

发送请求是异步的,但通过 .get()进行线程等待

核心实现

Lock+ Condition,通过 await()阻塞、signal()唤醒

好处

兼顾性能(异步发送)和易用性(同步获取结果)

1.8. Lock-Condition例子

示例功能:

  • 有一个共享队列
  • 生产者线程往队列里放数据;
  • 消费者线程从队列中取数据;
  • 使用 ReentrantLockCondition 实现线程间的等待和唤醒机制

Java代码:

import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class LockConditionExample {

    // 定义一个固定大小的共享队列
    private static final int CAPACITY = 5;
    private final Queue<Integer> queue = new LinkedList<>();

    // 显式锁对象
    private final Lock lock = new ReentrantLock();

    // 队列满时:生产者等待的条件
    private final Condition notFull = lock.newCondition();

    // 队列空时:消费者等待的条件
    private final Condition notEmpty = lock.newCondition();

    // 生产者方法
    public void produce(int value) throws InterruptedException {
        lock.lock(); // 获取锁
        try {
            // 如果队列已满,则等待 notFull 条件
            while (queue.size() == CAPACITY) {
                System.out.println("队列满了,生产者等待...");
                notFull.await(); // 进入等待并释放锁
            }

            // 加入元素到队列
            queue.offer(value);
            System.out.println("生产者生产了: " + value);

            // 通知消费者:队列非空
            notEmpty.signal();
        } finally {
            lock.unlock(); // 释放锁
        }
    }

    // 消费者方法
    public int consume() throws InterruptedException {
        lock.lock(); // 获取锁
        try {
            // 如果队列为空,则等待 notEmpty 条件
            while (queue.isEmpty()) {
                System.out.println("队列空了,消费者等待...");
                notEmpty.await(); // 进入等待并释放锁
            }

            // 从队列中取出一个元素
            int value = queue.poll();
            System.out.println("消费者消费了: " + value);

            // 通知生产者:队列不满了
            notFull.signal();
            return value;
        } finally {
            lock.unlock(); // 释放锁
        }
    }

    // 主函数 - 启动生产者和消费者线程
    public static void main(String[] args) {
        LockConditionExample example = new LockConditionExample();

        // 生产者线程
        Thread producer = new Thread(() -> {
            int value = 0;
            try {
                while (true) {
                    example.produce(value++);
                    Thread.sleep(500); // 模拟生产速度
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        // 消费者线程
        Thread consumer = new Thread(() -> {
            try {
                while (true) {
                    example.consume();
                    Thread.sleep(1000); // 模拟消费速度
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        // 启动线程
        producer.start();
        consumer.start();
    }
}

疑问:

它们共享一个锁,但它们仍然能「协作式」地工作。不是并行执行临界区的代码,而是轮流进入、协同完成任务。

这意味着:
同一时刻只能有一个线程(无论是生产者还是消费者)进入加锁的代码块,也就是说:

  • 生产者线程进入produce() 方法时,消费者线程如果也想进入 consume() 方法,就必须等待
  • 同样,反过来也一样。

那为什么说它们“可以协同工作”?

因为它们用 Condition 做了同步等待与唤醒

具体情况如下:

场景

行为

队列满时

生产者执行 notFull.await()→ 放弃锁 → 等待消费者消费后唤醒它

队列有空位了

消费者 signal()唤醒了 notFull.await()处的生产者

队列空时

消费者执行 notEmpty.await() → 放弃锁 → 等待生产者生产后唤醒它

队列有数据了

生产者 signal()唤醒了notEmpty.await()处的消费者

  • 所以虽然它们使用同一个锁,但通过Condition 的等待和唤醒机制,实现了“互不干扰”的轮流工作流程 —— 类似于两个人轮流进一个房间传递东西,每次只能一个人进去,但彼此不会阻碍工作流程

网站公告

今日签到

点亮在社区的每一天
去签到