深入解析 JDK Lock:为什么必须在同一线程加锁和解锁?

前言

在多线程编程中,锁是一种常用的机制,用于控制对共享资源的访问,防止竞态条件的出现。Java 中的 Lock 接口提供了比 synchronized 关键字更灵活的锁机制。我们通常会使用 Lock 来确保同一时刻只有一个线程能访问某个共享资源。但是,为什么 Lock 必须在同一个线程中加锁和解锁呢?

1. Lock 的基本概念

在 Java 中,Lock 接口位于 java.util.concurrent.locks 包中,常用的实现类包括 ReentrantLockReentrantReadWriteLock 等。与 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

为了确保程序的正确性,我们应遵循以下原则:

  1. 同一线程加锁和解锁:确保解锁操作在加锁的同一线程中执行。
  2. 使用 try-finally 语句 :通过 try-finally 确保锁能被正确释放,即使发生异常。
  3. 避免死锁:设计时注意锁的获取顺序,避免多个线程因交叉等待而死锁。
  4. 考虑中断和超时 :使用 tryLock()lockInterruptibly() 提高锁的灵活性,避免长时间等待。

5. 如何解决跨线程加锁和解锁的问题

5.1 使用信号量(Semaphore

信号量是一种计数工具,可控制访问资源的线程数量:

java 复制代码
Semaphore semaphore = new Semaphore(1); // 允许一个线程访问
try {
    semaphore.acquire(); // 获取许可
    // 执行临界区代码
} finally {
    semaphore.release(); // 释放许可
}

5.2 使用 CountDownLatchCyclicBarrier

这些工具类可协调线程间的执行顺序,从而避免跨线程解锁的错误。

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 使用分布式锁

在分布式场景下,使用分布式锁(如基于 RedisRedisson):

java 复制代码
RedissonClient redisson = Redisson.create();
RLock lock = redisson.getLock("myLock");
lock.lock();
try {
    // 执行操作
} finally {
    lock.unlock();
}

6. 总结

在使用 Lock 时,根据是否为同一线程加锁和解锁,可以选择不同的实现方案:

6.1 同一线程加锁和解锁

适用于大部分场景,如需要灵活控制锁的获取和释放、优化性能、或复杂的同步逻辑时,使用 ReentrantLockReentrantReadWriteLock 是最佳选择。

6.2 不同线程加锁和解锁

Lock 不支持跨线程解锁。如果需要跨线程管理锁,可以选择其他同步工具:

  • 信号量(Semaphore:用于控制资源访问数量。
  • CountDownLatchCyclicBarrier:协调线程间的执行顺序。
  • 分布式锁:适用于分布式系统中的资源同步需求。

合理选择锁的类型和机制,可以确保程序的正确性和高效性。


结语

感谢阅读本篇文章!希望通过对 Lock 使用场景的解析和总结,能够帮助大家更好地理解并发编程中的锁机制。

如果此文章对您有帮助💪,帮我点个赞👍,感激不尽🤝!"

相关推荐
一条小小yu8 分钟前
从零开始手写缓存之如何实现固定缓存大小
java·spring·缓存
天之涯上上13 分钟前
JAVA开发中 MyBatis XML 映射文件 的作用
xml·java·mybatis
鸿永与1 小时前
『SQLite』常见数据类型(动态类型系统)
java·数据库·sqlite
一弓虽1 小时前
java基础学习——java泛型
java·学习
我真不会起名字啊2 小时前
QtJson数据格式处理详解
java·前端·javascript
硕风和炜2 小时前
【LeetCode: 112. 路径总和 + 二叉树 + 递归】
java·算法·leetcode·面试·二叉树·递归
Xwzzz_3 小时前
基于Redisson实现重入锁
java·redis·lua
吴冰_hogan3 小时前
并发编程之CAS与Atomic原子操作详解
java·开发语言·数据库
YaHuiLiang3 小时前
小微互联网公司与互联网创业公司的技术之殇 - "新"技术的双刃剑
前端·后端·架构
Young丶3 小时前
SpringBoot Maven 项目 pom 中的 plugin 插件用法整理
spring boot·后端·maven