前言
在多线程编程中,锁是一种常用的机制,用于控制对共享资源的访问,防止竞态条件的出现。Java 中的
Lock
接口提供了比synchronized
关键字更灵活的锁机制。我们通常会使用Lock
来确保同一时刻只有一个线程能访问某个共享资源。但是,为什么 Lock 必须在同一个线程中加锁和解锁呢?
1. Lock 的基本概念
在 Java 中,Lock
接口位于 java.util.concurrent.locks
包中,常用的实现类包括 ReentrantLock
和 ReentrantReadWriteLock
等。与 synchronized
相比,Lock
提供了以下优势:
- 支持尝试获取锁 (
tryLock()
):允许线程在锁不可用时采取其他措施。 - 支持中断获取锁 (
lockInterruptibly()
):可以响应中断的锁获取操作。 - 支持定时锁 (
lock(long time, TimeUnit unit)
):允许线程尝试在给定时间内获取锁。 - 支持公平锁和非公平锁:用户可以选择锁的获取策略。
Lock
的使用通常需要手动调用 lock()
方法来获取锁,并在使用完后调用 unlock()
方法释放锁。
2. Lock 为什么必须在同一线程中加锁和解锁
Lock
要求锁必须由加锁的线程来解锁,这是为了确保锁的所有权和程序的稳定性。以下是主要原因:
2.1 锁的所有权
每个线程在获取锁时会成为锁的持有者,只有持有者才能解锁。如果不同线程尝试解锁,会导致 IllegalMonitorStateException
异常。
2.2 避免死锁
如果允许锁在不同线程间解锁,可能会导致死锁。例如,多个线程互相等待彼此释放锁,程序将陷入无法继续执行的状态。
2.3 锁的可重入性
即使是可重入锁,其计数也只能在同一个线程内增加和减少。不同线程解锁会导致锁计数错误,最终引发程序错误。
3. 代码验证:Lock 是否必须在同一线程中加锁和解锁
通过以下代码可以验证 Lock 的这一特性。
3.1 在同一线程中加锁和解锁
测试代码如下:
java
@RestController
public class TestController {
private static final Lock lock = new ReentrantLock();
@GetMapping("testLock")
public String testLock() throws InterruptedException {
System.out.println("开始访问");
if (lock.tryLock()) {
try {
System.out.println("获取到锁,执行业务逻辑");
// 模拟执行
Thread.sleep(5000L);
System.out.println("执行完毕");
return "访问成功";
} finally {
lock.unlock();
}
} else {
System.out.println("未获取到锁,强制结束方法");
return "未获取到锁,强制结束方法";
}
}
}
浏览器访问接口时,加锁与解锁正常进行:
3.2 在不同线程中加锁和解锁
测试代码如下:
java
@RestController
public class TestController {
private static final Lock lock = new ReentrantLock();
@GetMapping("testLock2")
public String testLock2() {
System.out.println("开始访问");
if (lock.tryLock()) {
// 开启线程
new Thread(() -> {
try {
Thread.sleep(5000L);
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
try {
lock.unlock();
} catch (Exception e) {
System.out.println("释放锁失败");
e.printStackTrace();
}
}
}).start();
return "访问成功";
} else {
System.out.println("未获取到锁,强制结束方法");
return "未获取到锁,强制结束方法";
}
}
}
浏览器访问时发现 5 秒过去了,但锁依然未释放:
控制台显示 IllegalMonitorStateException
异常:
4. 如何正确使用 Lock
为了确保程序的正确性,我们应遵循以下原则:
- 同一线程加锁和解锁:确保解锁操作在加锁的同一线程中执行。
- 使用 try-finally 语句 :通过
try-finally
确保锁能被正确释放,即使发生异常。 - 避免死锁:设计时注意锁的获取顺序,避免多个线程因交叉等待而死锁。
- 考虑中断和超时 :使用
tryLock()
或lockInterruptibly()
提高锁的灵活性,避免长时间等待。
5. 如何解决跨线程加锁和解锁的问题
5.1 使用信号量(Semaphore
)
信号量是一种计数工具,可控制访问资源的线程数量:
java
Semaphore semaphore = new Semaphore(1); // 允许一个线程访问
try {
semaphore.acquire(); // 获取许可
// 执行临界区代码
} finally {
semaphore.release(); // 释放许可
}
5.2 使用 CountDownLatch
或 CyclicBarrier
这些工具类可协调线程间的执行顺序,从而避免跨线程解锁的错误。
5.2.1 使用 CountDownLatch
java
CountDownLatch latch = new CountDownLatch(1);
// 等待信号
latch.await();
// 发送信号
latch.countDown();
5.2.1 使用 CyclicBarrier
java
CyclicBarrier barrier = new CyclicBarrier(2);
// 等待线程
barrier.await();
5.3 使用分布式锁
在分布式场景下,使用分布式锁(如基于 Redis
的 Redisson
):
java
RedissonClient redisson = Redisson.create();
RLock lock = redisson.getLock("myLock");
lock.lock();
try {
// 执行操作
} finally {
lock.unlock();
}
6. 总结
在使用 Lock
时,根据是否为同一线程加锁和解锁,可以选择不同的实现方案:
6.1 同一线程加锁和解锁
适用于大部分场景,如需要灵活控制锁的获取和释放、优化性能、或复杂的同步逻辑时,使用 ReentrantLock
或 ReentrantReadWriteLock
是最佳选择。
6.2 不同线程加锁和解锁
Lock
不支持跨线程解锁。如果需要跨线程管理锁,可以选择其他同步工具:
- 信号量(
Semaphore
):用于控制资源访问数量。 CountDownLatch
或CyclicBarrier
:协调线程间的执行顺序。- 分布式锁:适用于分布式系统中的资源同步需求。
合理选择锁的类型和机制,可以确保程序的正确性和高效性。
结语
感谢阅读本篇文章!希望通过对
Lock
使用场景的解析和总结,能够帮助大家更好地理解并发编程中的锁机制。如果此文章对您有帮助💪,帮我点个赞👍,感激不尽🤝!"