Java中的Lock使用实例详解
作者:Arva .
1. 为什么需要 Lock?(从 synchronized 说起)
synchronized 的局限性:synchronized 用起来很简单,但它有一些“笨拙”的地方:
- 锁的获取和释放是固定的:线程进入
synchronized块时获取锁,退出时(无论是正常退出还是异常退出)自动释放锁。你无法手动控制。 - 不可中断:如果一个线程在等待锁,它会一直等下去,不能被中断。
- 非公平锁:默认情况下,等待的线程们不是按先来后到的顺序获取锁的,可能会“插队”。
- 只有一个条件队列:等待和通知用的是
wait()和notify(),不够灵活。
为了解决这些“笨拙”的问题,Java 在 1.5 版本引入了 java.util.concurrent.locks.Lock 接口。
2. Lock 是什么?
Lock像一个需要你用钥匙开闭的密码锁或指纹锁:你可以自己决定什么时候上锁,什么时候解锁。功能强大,可以设置密码(尝试获取锁)、设置超时时间等,但需要你自己管理钥匙,别弄丢了。
3. Lock 的核心方法
Lock 接口最主要的方法有以下几个:
lock(): 获取锁。如果锁被其他线程占用,则当前线程会一直等待,直到拿到锁为止。这是最基础的用法。unlock(): 释放锁。非常重要! 你必须手动调用,通常放在finally块中以确保一定会被执行,防止死锁。tryLock(): 尝试获取锁。它不会像lock()那样死等。如果能拿到锁,就返回true;如果拿不到,就立刻返回false。这给了你“抢不到就去做别的事”的可能性。tryLock(long time, TimeUnit unit): 带超时的尝试获取锁。在指定的时间内尝试获取锁,获取成功则返回true,超时后还获取不到则返回false。这个方法非常实用,可以避免线程无限期等待。lockInterruptibly(): 可中断地获取锁。在等待锁的过程中,线程可以被中断(调用interrupt()方法)。
4. 最常用的实现类:ReentrantLock
ReentrantLock 是 Lock 接口最主要、最常用的实现类。它的名字叫“可重入锁”,意思是同一个线程可以多次获取同一把锁而不会把自己锁死(和 synchronized 一样)。
基础使用模板
使用 Lock 有一个固定的“套路”,以确保锁一定能被释放:
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
// 1. 创建一个 Lock 实例,通常是 ReentrantLock
private final Lock lock = new ReentrantLock();
public void safeMethod() {
// 2. 在操作共享资源前,获取锁
lock.lock();
try {
// 3. 在这里执行需要线程安全的代码(临界区代码)
// ... 比如修改一个共享变量
System.out.println(Thread.currentThread().getName() + " 拿到了锁");
} finally {
// 4. 在 finally 块中释放锁,确保无论发生什么异常,锁都会被释放
lock.unlock();
System.out.println(Thread.currentThread().getName() + " 释放了锁");
}
}
}关键点: lock.unlock() 一定要放在 finally 块里!否则如果临界区代码发生异常,锁可能永远无法释放,导致其他线程全部“饿死”。
高级用法示例:tryLock
public void tryLockExample() {
if (lock.tryLock()) { // 尝试获取锁
try {
// 成功拿到锁,执行任务
System.out.println(Thread.currentThread().getName() + " 成功获取锁,执行任务...");
Thread.sleep(1000); // 模拟任务执行时间
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
} else {
// 没拿到锁,去做别的事情
System.out.println(Thread.currentThread().getName() + " 获取锁失败,去执行其他任务了...");
}
}ReentrantLock基于AQS,AQS有tryAcquire(),所以ReentrantLock有tryLock()尝试加锁
5. Lock 的另一个强大功能:条件(Condition)
synchronized 的等待/通知
在 synchronized 中,我们是这样让线程等待和唤醒的:
synchronized (obj) {
while (条件不满足) {
obj.wait(); // 线程等待
}
// 执行任务...
}
// 另一个线程中
synchronized (obj) {
obj.notify(); // 唤醒一个等待的线程
// 或者 obj.notifyAll(); 唤醒所有等待的线程
}问题:所有线程都在同一个"等待队列"里,我们无法精确控制唤醒哪种线程。
Condition 就像"专门的等待室"
想象一个餐厅:
- synchronized:只有一个大的"等候区",所有客人都挤在一起
- Condition:有多个专门的"等候室"
notFull:等待"桌子有空位"的生产者等候室notEmpty:等待"有菜可吃"的消费者等候室
代码对比理解
import java.util.concurrent.locks.*;
public class Restaurant {
private final Lock lock = new ReentrantLock();
// 创建两个专门的"等候室"
private final Condition notFull = lock.newCondition(); // "桌子有空位"等候室
private final Condition notEmpty = lock.newCondition(); // "有菜可吃"等候室
private int foodCount = 0;
private final int MAX_FOOD = 5;
// 厨师(生产者):做菜
public void cook() throws InterruptedException {
lock.lock();
try {
while (foodCount == MAX_FOOD) {
System.out.println("厨房满了,厨师在notFull等候室等待...");
notFull.await(); // 如果厨房满了,就去"桌子有空位"等候室等待
}
// 做菜
foodCount++;
System.out.println("厨师做了一道菜,现在有 " + foodCount + " 道菜");
// 通知在"有菜可吃"等候室等待的客人
notEmpty.signal();
} finally {
lock.unlock();
}
}
// 客人(消费者):吃菜
public void eat() throws InterruptedException {
lock.lock();
try {
while (foodCount == 0) {
System.out.println("没菜了,客人在notEmpty等候室等待...");
notEmpty.await(); // 如果没菜了,就去"有菜可吃"等候室等待
}
// 吃菜
foodCount--;
System.out.println("客人吃了一道菜,还剩 " + foodCount + " 道菜");
// 通知在"桌子有空位"等候室等待的厨师
notFull.signal();
} finally {
lock.unlock();
}
}
}synchronized 配合 wait() 和 notify() 可以实现线程间的等待/通知机制。Lock 也有对应的、但更强大的功能,那就是 Condition。
你可以通过 Lock 的 newCondition() 方法创建一个 Condition 对象。一个 Lock 可以创建多个 Condition,这意味着你可以有多个等待队列,实现更精细的线程控制。
await(): 类似Object.wait(),让当前线程等待。signal(): 类似Object.notify(),唤醒一个在此 Condition 上等待的线程。signalAll(): 类似Object.notifyAll(),唤醒所有在此 Condition 上等待的线程。
经典场景:生产者-消费者模型
你可以创建两个 Condition:notFull(队列未满)和 notEmpty(队列未空)。
- 生产者往队列放东西前,如果队列满了,就在
notFull上等待;放完之后,就唤醒在notEmpty上等待的消费者。 - 消费者从队列拿东西前,如果队列空了,就在
notEmpty上等待;拿完之后,就唤醒在notFull上等待的生产者。
这样比单一的 wait()/notify() 更清晰,不容易出错。
6. 公平锁 vs 非公平锁
在创建 ReentrantLock 时,可以传入一个 boolean 参数来指定是否是公平锁:
new ReentrantLock(true): 公平锁。线程们按申请锁的先后顺序来获取锁,先到先得。new ReentrantLock(false): 非公平锁(默认)。允许“插队”,后申请的线程可能比先申请的线程先拿到锁。
AQS先进先出的队列为实现公平锁奠定了基础。ReeentrantLock中有一个FairSync和NonfairSync的子类,他们都重写了AQS的tryAcquire()方法,FairSync是依赖了AQS的队列结构,实现了先来先获取锁的操作。NonfairSync就允许新线程插队抢锁。
优缺点:
- 公平锁:优点是不会产生线程饥饿(某个线程永远拿不到锁)。缺点是吞吐量低,性能开销大。
- 非公平锁:优点是吞吐率高(减少了线程切换的开销)。缺点是可能导致线程饥饿。
在绝大多数情况下,使用默认的非公平锁即可,因为性能更好。
7. 总结:Lock 和 synchronized 如何选择?
| 特性 | synchronized | Lock |
|---|---|---|
| 本质 | Java 语言关键字,内置的 | 是一个接口,需要手动实例化 |
| 用法 | 简单,自动加锁解锁 | 灵活,手动控制,需配合 try-finally |
| 可中断 | 否 | 是 (lockInterruptibly) |
| 超时获取 | 否 | 是 (tryLock(long, TimeUnit)) |
| 公平性 | 非公平 | 两者都可选(默认非公平) |
| ** Condition ** | 单一 | 多个 |
选择建议:
- 优先使用 synchronized:如果你的需求很简单,用
synchronized就足够了。因为它写起来更简洁,不容易出错(不会忘记释放锁),并且 JVM 对其有持续的优化。 - 当需要以下高级功能时,才使用
Lock:- 需要尝试获取锁(
tryLock)。 - 需要可中断的锁。
- 需要超时获取锁。
- 需要实现公平锁。
- 需要多个 Condition 来实现复杂的线程协作(比如复杂的生产者-消费者模型)。
- 需要尝试获取锁(
到此这篇关于Java中Lock使用详解的文章就介绍到这了,更多相关Java中Lock使用内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!
