JUC下的Java java.util.concurrent.Locks详解

java.util.concurrent.locks 包介绍

java.util.concurrent.locks 包是Java并发编程中非常重要的一个部分,它提供了比内置synchronized关键字更为灵活的锁机制,用于多线程环境下的同步控制。这个包中最核心的是Lock接口,以及一系列实现类,如ReentrantLock, ReentrantReadWriteLock, StampedLock, 和Condition接口等。

详细介绍

java.util.concurrent.locks 是Java并发编程包中的一个重要组成部分,它提供了一组高级的线程同步工具,这些工具比传统的synchronized关键字更加强大和灵活。这一包的核心是Lock接口及其相关的实现类,如ReentrantLock, ReentrantReadWriteLock, StampedLock, 以及与之配套的Condition接口。

核心组件的详细讲解:

Lock接口

Lock接口是整个包的核心,它定义了一组锁操作,包括获取锁、释放锁、尝试获取锁(可立即返回)、超时获取锁(在指定时间内尝试获取锁)以及可中断获取锁(在等待锁的过程中可以响应中断)。相比于synchronizedLock提供了更多的灵活性和控制权,比如:

  • 可中断的锁等待lockInterruptibly()方法允许等待锁的线程响应中断,而不会像synchronized那样一直等待下去。
  • 尝试非阻塞的获取锁tryLock()方法尝试获取锁,如果当前锁不可用,立即返回false,不会阻塞线程。
  • 超时获取锁tryLock(long time, TimeUnit unit)在指定的时间内尝试获取锁,超时后返回false
ReentrantLock

ReentrantLockLock接口的一个可重入实现,它支持公平锁和非公平锁两种模式。可重入意味着持有锁的线程可以再次获取该锁而不会造成死锁。公平锁按照线程请求锁的顺序分配锁,而非公平锁则允许插队,通常性能更高但可能导致某些线程“饥饿”。

ReentrantReadWriteLock

ReentrantReadWriteLock实现了读写锁,它允许多个读取者同时访问共享资源,但只允许一个写入者,并且写入者会排斥所有的读取者和其他写入者。这种设计特别适用于读多写少的并发场景,可以显著提高系统的并发性能。

StampedLock

StampedLock是Java 8引入的一种新型锁,提供了三种锁模式:读锁、写锁和乐观读锁。乐观读锁是一种特殊模式,它允许在没有真正加锁的情况下进行读取操作,然后通过版本戳(stamp)来检查读取期间是否有写操作发生,从而提供了一种既能够提高并发性能又能保证数据一致性的灵活机制。

Condition接口

Condition接口与锁配合使用,作为Object类中waitnotifynotifyAll方法的替代品,提供了更精细的线程协调能力。每个Lock实例都有一个或多个与之关联的Condition实例,线程可以通过await方法等待某个条件,而其他线程可以通过signalsignalAll方法唤醒等待的线程。

使用场景

java.util.concurrent.locks包中的工具在Java并发编程中扮演着至关重要的角色,它们在多种场景下展现出独特的价值和优势。

几个典型使用场景:

1. 替代synchronized关键字

synchronized关键字无法满足需求时,例如需要更细粒度的控制或更高级的锁特性,可以使用Lock接口及其实现类。例如,如果需要在等待锁时能够响应中断,可以使用ReentrantLocklockInterruptibly()方法,这是synchronized不具备的特性。

2. 读写分离场景

在读多写少的并发场景下,使用ReentrantReadWriteLock可以显著提升性能。读锁可以被多个线程同时持有,而写锁独占,确保了写操作的原子性和一致性。例如,在数据库缓存、配置文件读取等场景,可以大幅度提高并发读取的效率。

3. 复杂同步逻辑

当同步逻辑较为复杂,需要更精细的线程协调时,Condition接口提供了强大的功能。例如,在生产者消费者模型中,生产者可以使用condition.signal()通知等待的消费者线程,而消费者则通过condition.await()等待产品就绪,这样的机制比简单的wait()notify()更为灵活和精确。

4. 乐观锁机制

在数据竞争不太激烈的场景下,StampedLock的乐观读锁模式可以提供非常高的并发性能。例如,在构建缓存系统时,大多数时候都是读操作,乐观锁可以在不阻塞其他线程的情况下进行读取,只有在极少数情况下(写操作)才需要加锁,这大大提高了系统的吞吐量。

5. 可重入锁的需求

当一个线程需要多次获取同一个锁时,无论是递归调用还是在不同的代码块中,使用ReentrantLockReentrantReadWriteLock可以避免死锁。这些锁会跟踪锁的持有者和持有次数,只有当持有次数降为零时才会真正释放锁。

6. 超时与中断处理

在需要控制线程等待时间或响应中断的场景,如网络连接尝试、资源访问限时等待等,可以利用Lock接口的tryLock(long time, TimeUnit unit)方法,该方法允许在指定时间内尝试获取锁,并且支持在等待过程中响应中断。

7. 高性能并发容器

在自定义高性能并发容器时,可以利用这些锁来实现线程安全。例如,结合ReentrantLockStampedLock实现线程安全的队列、栈或映射表,可以精确控制同步点,提升整体性能。

实际开发中的使用详情与注意事项

使用详情
  1. 选择合适的锁类型

    • 对于一般的互斥需求,可以选择ReentrantLock,它提供了可重入能力,以及比synchronized更灵活的控制方式。
    • 在读多写少的场景,推荐使用ReentrantReadWriteLock,它能显著提高并发读取的效率。
    • 对于需要更高级功能,如乐观读锁,可以选择StampedLock,但要注意它的复杂性和风险。
  2. 正确管理锁的生命周期

    • 获取锁:确保在需要同步的代码块前正确获取锁,对于Lock接口,通常使用lock()tryLock()方法。
    • 释放锁:无论同步代码块执行结果如何,都必须确保锁被释放,推荐在finally块中调用unlock()方法。
  3. 异常处理

    • 在使用lockInterruptibly()时,要准备处理InterruptedException,确保线程状态的正确恢复。
  4. 使用Condition进行线程协作

    • 当需要基于条件的线程等待和通知时,使用Lock对象的newCondition()方法创建Condition对象,并通过await()signal()/signalAll()方法进行线程间的协调。
  5. 性能监控与调优

    • 利用锁实现类提供的统计信息(如ReentrantLock.getHoldCount()),监控锁的使用情况,辅助性能调优。
注意事项
  1. 避免死锁
    • 确保获取锁的顺序一致,避免循环等待。使用锁时,遵循一定的顺序规则,或者采用锁顺序死锁预防算法。
  2. 避免活锁和饥饿
    • 在使用tryLock()时,要小心设计重试逻辑,避免无休止的重试形成活锁。对于写锁,考虑公平性设置,以减少线程饥饿现象。
  3. 合理选择公平性
    • ReentrantLockReentrantReadWriteLock都支持公平与非公平模式。公平模式下,线程按照请求锁的顺序获得锁,保证了公平性,但可能降低并发性能;非公平模式下,允许插队,提高吞吐量,但可能导致某些线程长时间等待。
  4. 资源清理
    • 在不再需要锁对象时,确保及时清理,避免资源泄露。
  5. 测试与验证
    • 并发代码的测试尤为重要,使用单元测试和压力测试来确保锁逻辑正确无误,特别是并发场景下的行为符合预期。
  6. 文档与注释
    • 在使用锁的地方,尤其是复杂的锁交互逻辑,做好详细的代码注释,解释锁的作用、获取和释放逻辑,方便后续维护和团队协作。

优缺点

优点
  1. 灵活性与控制性:相较于synchronized关键字,java.util.concurrent.locks提供了更精细的控制能力,比如可中断的锁等待、尝试非阻塞获取锁、超时获取锁等,这让开发者能够根据实际需求设计更复杂的并发控制逻辑。

  2. 性能优化:特别是ReentrantReadWriteLockStampedLock,通过区分读写操作,能够显著提高并发读取的性能。在读多写少的场景下,读锁可以被多个线程同时持有,减少了锁的竞争,提高了系统吞吐量。

  3. 可重入性ReentrantLockReentrantReadWriteLock都支持可重入特性,允许持有锁的线程再次获取该锁而不会造成死锁,这对于需要在锁保护的代码块内部调用自身或调用其他需要相同锁的方法非常有用。

  4. 条件队列Condition接口提供了比传统wait/notify/notifyAll更精细的线程协调机制,允许在多个条件上进行等待和通知,有助于实现更复杂的并发控制逻辑。

  5. 公平性选择ReentrantLockReentrantReadWriteLock提供了公平锁和非公平锁的选择,允许开发者根据应用场景的需要平衡性能与公平性。

缺点
  1. 复杂性:相较于使用synchronized,直接使用java.util.concurrent.locks的API需要编写更多的代码,增加了程序的复杂度,对开发者的要求更高,尤其是在锁的管理和异常处理方面。

  2. 容易出错:手动管理锁的获取和释放(尤其是在异常处理中确保锁被正确释放)增加了潜在的错误风险,如忘记解锁可能导致死锁或资源泄露。

  3. 性能开销:虽然在很多场景下,这些高级锁可以提供更好的性能,但是不当使用,如过度频繁的锁获取和释放,或在不需要同步的代码块上使用锁,反而可能引入不必要的性能开销。

  4. 学习曲线:理解和熟练运用这些高级锁机制需要时间和实践,尤其是StampedLock,其提供的乐观读锁特性虽然强大,但使用不当可能导致难以调试的并发问题。

代码示例

示例1:使用ReentrantLock进行基本的锁操作
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class ReentrantLockExample {private final Lock lock = new ReentrantLock();public void criticalSectionOperation() {lock.lock(); // 获取锁try {// 临界区代码System.out.println(Thread.currentThread().getName() + " executing critical section.");Thread.sleep(1000); // 模拟耗时操作} catch (InterruptedException e) {Thread.currentThread().interrupt();System.out.println("Thread interrupted.");} finally {lock.unlock(); // 确保锁被释放}}public static void main(String[] args) {ReentrantLockExample example = new ReentrantLockExample();Thread t1 = new Thread(() -> example.criticalSectionOperation(), "Thread-1");Thread t2 = new Thread(() -> example.criticalSectionOperation(), "Thread-2");t1.start();t2.start();}
}

 此例展示了如何使用ReentrantLock来保护临界区代码,确保一次只有一个线程能够执行这段代码。通过lock()获取锁,unlock()释放锁,并在finally块中释放锁以确保即使发生异常锁也能被正确释放。

示例2:使用ReentrantReadWriteLock实现读写分离
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;public class ReadWriteLockExample {private final Map<String, String> data = new HashMap<>();private final ReadWriteLock readWriteLock = new ReentrantReadWriteLock();public void putData(String key, String value) {readWriteLock.writeLock().lock();try {System.out.println(Thread.currentThread().getName() + " writing data.");data.put(key, value);Thread.sleep(1000); // 模拟写操作耗时} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {readWriteLock.writeLock().unlock();}}public String getData(String key) {readWriteLock.readLock().lock();try {System.out.println(Thread.currentThread().getName() + " reading data.");return data.get(key);} finally {readWriteLock.readLock().unlock();}}public static void main(String[] args) {ReadWriteLockExample example = new ReadWriteLockExample();Thread writer1 = new Thread(() -> example.putData("key1", "value1"), "Writer-1");Thread reader1 = new Thread(() -> System.out.println(example.getData("key1")), "Reader-1");Thread reader2 = new Thread(() -> System.out.println(example.getData("key1")), "Reader-2");writer1.start();reader1.start();reader2.start();}
}

这个例子展示了如何使用ReentrantReadWriteLock来实现读写锁,允许多个读取者同时访问,但只允许一个写入者,并且写入时排斥所有读取者。这在读多写少的场景下能够显著提升并发性能。

示例3:使用Condition进行线程间协作
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class ConditionExample {private final Lock lock = new ReentrantLock();private final Condition condition = lock.newCondition();private boolean flag = false;public void before() {lock.lock();try {while (!flag) {System.out.println(Thread.currentThread().getName() + " waiting...");condition.await(); // 等待条件满足}System.out.println(Thread.currentThread().getName() + " continuing...");} catch (InterruptedException e) {Thread.currentThread().interrupt();} finally {lock.unlock();}}public void after() {lock.lock();try {flag = true;condition.signalAll(); // 唤醒所有等待的线程System.out.println("Signal sent by " + Thread.currentThread().getName());} finally {lock.unlock();}}public static void main(String[] args) {ConditionExample example = new ConditionExample();Thread waiter = new Thread(() -> example.before(), "Waiter");Thread notifier = new Thread(() -> example.after(), "Notifier");waiter.start();// 确保waiter线程先开始等待try { Thread.sleep(1000); } catch (InterruptedException e) {}notifier.start();}
}

此例演示了如何使用Condition来进行线程间的协调。一个线程(Waiter)等待特定条件(flag变为true),另一个线程(Notifier)改变条件后通过signalAll()唤醒所有等待的线程。await()signalAll()方法的使用展示了比传统的wait()notifyAll()更为灵活和精确的线程控制。

可能遇到的问题及解决方案

1. 死锁

问题描述:当两个或更多的线程互相等待对方释放锁,形成循环等待的情况,就会发生死锁。

解决方案

  • 避免嵌套锁:尽量减少锁的嵌套使用,或者确保在所有线程中按照相同的顺序获取锁。
  • 使用超时:在尝试获取锁时使用带有超时的tryLock(long time, TimeUnit unit)方法,超时后放弃锁请求,打破死锁循环。
  • 检测死锁:使用ThreadMXBean检测死锁,一旦发现,采取相应措施如中断相关线程。
2. 锁泄露

问题描述:如果在异常情况下没有正确释放锁,可能导致锁泄露,进而引起资源耗尽或系统不稳定。

解决方案

  • 在finally块中释放锁:确保无论是否发生异常,锁都能在finally块中被释放。
  • 使用try-with-resources:对于支持AutoCloseable的锁实现(如ReentrantLockLock包装器java.util.concurrent.locks.Lockclose方法),可以使用try-with-resources语句自动管理锁的生命周期。
3. 性能问题

问题描述:不恰当的锁使用可能引入性能瓶颈,如过度同步、锁粒度过大、频繁的锁获取和释放等。

解决方案

  • 最小化锁的范围:确保锁只保护必要的代码块,减少锁的持有时间。
  • 使用读写锁:在读多写少的场景下,使用ReentrantReadWriteLock代替独占锁,提高并发读取性能。
  • 评估乐观锁的使用:在适当场景下,考虑使用StampedLock的乐观读锁模式,减少锁的争用。
4. 条件不匹配导致的等待无限期

问题描述:使用Condition等待特定条件时,如果没有机制确保条件最终会被满足,线程可能会永久等待。

解决方案

  • 定期检查条件:在循环中调用await(),并在循环外部检查条件是否满足,避免无条件等待。
  • 使用信号量或计数器:在某些场景下,使用Semaphore或原子计数器作为条件满足的信号,确保信号能够被正确发送和接收。
5. 公平性与吞吐量的权衡

问题描述:公平锁虽然保证了线程的公平性,但可能降低了系统的整体吞吐量。

解决方案

  • 根据场景选择锁策略:在性能敏感且对公平性要求不高的场景,使用非公平锁;而在需要保证线程调度公平性的场景,选择公平锁。
  • 动态调整锁策略:根据运行时的负载情况,动态切换锁的公平性设置(虽然Guava本身不直接支持,但可以根据业务逻辑设计自定义的锁选择策略)。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/11324.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何通过香港站群服务器高效实现网站内容的快速更新?

如何通过香港站群服务器高效实现网站内容的快速更新? 在当今激烈的数字市场竞争中&#xff0c;网站内容的快速更新对于吸引用户和保持竞争优势至关重要。而利用香港站群服务器实现这一目标&#xff0c;则具备诸多优势。下面将详细探讨如何通过香港站群服务器高效实现网站内容…

thinkphp6使用layui分页组件做分页效果

博主用的是layui2.9.8的版本&#xff0c;但这个版本的分页组件是动态效果的&#xff0c;但我需要的是静态分页&#xff0c;所以我自己封装了一个生成layui的分页代码生成代码。代码如下&#xff1a; 1、先创建文件&#xff0c;路径是extent/layui/LayuiPage.php&#xff0c;加…

基于MetaGPT的LLM Agent学习实战(一)

前言 我最近一直在做基于AI Agent 的个人项目&#xff0c; 因为工作加班较多&#xff0c;设计思考时间不足&#xff0c;这里借着Datawhale的开源学习课程《MetaGPT智能体理论与实战》课程&#xff0c;来完善自己的思路&#xff0c;抛砖引玉&#xff0c;和各位开发者一起学习&am…

Run ‘conda init‘ before ‘conda activate‘

使用conda activate 虚拟环境名称的时候提示&#xff1a;Run conda init before conda activate 解决办法&#xff1a; 首先需要确保是管理员身份运行这个cmd窗口。 然后&#xff0c;现在执行一下&#xff1a;conda init 命令&#xff0c;最后再执行&#xff1a;conda activate…

纯血鸿蒙APP实战开发——首页下拉进入二楼效果案例

介绍 本示例主要介绍了利用position和onTouch来实现首页下拉进入二楼、二楼上划进入首页的效果场景&#xff0c;利用translate和opacity实现动效的移动和缩放&#xff0c;并将界面沉浸式&#xff08;全屏&#xff09;显示。 效果图预览 使用说明 向下滑动首页页面超过触发距…

冯喜运:5.13黄金原油震荡整理是涨还是跌?今日走势分析

【黄金消息面分析】;自5月初以来&#xff0c;黄金和白银一直在享受需求的回归&#xff0c;买家在过去几天加大了力度&#xff0c;一度推动金价重返2370美元上方&#xff0c;白银重返28.5美元上方。不过&#xff0c;经过几天的盘整后&#xff0c;黄金白银价格双双下跌。然而&…

Redis-详解(基础)

文章目录 什么是Redis&#xff1f;用Redis的特点&#xff1f;用Redis可以实现哪些功能&#xff1f;Redis的常用数据类型有哪些?Redis的常用框架有哪些?本篇小结 更多相关内容可查看 什么是Redis&#xff1f; Redis&#xff08;Remote DictionaryServer&#xff09;是一个开源…

下载文件名称乱码或变成了随机码

如图 后端是有正常返回附件名称的,浏览器开发工具中也正常显示了这个数据,但是下载下来的文件名称确实一堆随机码. 其实这个问题的原因是因为跨域 查看console: Refused to get unsafe header "content-disposition" 现象,后端传递到前端的fileName不能被识别,下载…

大模型与AIGC应用相关问题 模型大型

最近经常被问&#xff0c;你看“万亿的模型都出来了&#xff0c;你们训练的千亿模型是不是落伍了&#xff1f;”我想说&#xff1a;“虽然都叫超大模型&#xff0c;但是类型是不一样的&#xff0c;虽说每一类模型训出来都不容易&#xff0c;不过澄清一下概念还是必要的”。 大…

编写Ansible角色实现分布式LNMP安装

前言 本文将介绍如何使用 Ansible 编写角色&#xff0c;在分布式环境下完成 LNMP&#xff08;Linux、Nginx、MySQL、PHP&#xff09;的自动化&#xff08;编译&#xff09;安装和配置&#xff0c;并验证 PHP 与 MySQL 数据联通性&#xff0c;实现博客和论坛页面的展示。 常规…

Qt---信号和槽

一、信号和槽机制 所谓信号槽&#xff0c;实际就是观察者模式。当某个事件发生之后&#xff0c;比如&#xff0c;按钮检测到自己被点击了一下&#xff0c;它就会发出一个信号&#xff08;signal&#xff09;。这种发出是没有目的的&#xff0c;类似广播。如果有对象对这个信号…

二叉树的四种遍历代码实现

二叉树的遍历大致能分为以下几种 1.前序&#xff1a;根 左 右 2.中序&#xff1a;左 根 右 3.后序&#xff1a;左 右 根 4.层序&#xff1a;从根开始一层一层的向下 如上图访问顺序: 前序&#xff1a;1 2 3 N N N 4 5 N N 6 N N 中序&#xff1a;N 3 N 2 N 1 N 5 N 4 N …

docker-compose安装emqx集群(最新)(host模式)

机器&#xff1a; 10.60.0.20 10.60.0.21 10.60.0.22 一、三台机子都配置域名&#xff08;/etc/hosts&#xff09; 10.60.0.20 node1.emqx.io 10.60.0.22 node3.emqx.io 10.60.0.21 node2.emqx.io 二、docker-compose.yml&#xff08;10.60.0.21&#xff09; 其他两台机子自…

接搭建仿美团、代付系统源码搭建教程

最近很多粉丝催更、分享一下地球号&#xff1a;xiaobao0214520(WX) 现在大家都很流行搞网恋&#xff0c;我们搭建一个跟美团相似的系统 然后开发一个好友代付&#xff0c;我们在点单的时候转发链接让网恋对象付钱 若只是单点外卖的话&#xff0c;能榨出的油水还是太少。 所以…

Golang — map的使用心得和底层原理

map作为一种基础的数据结构&#xff0c;在算法和项目中有着非常广泛的应用&#xff0c;以下是自己总结的map使用心得、实现原理、扩容机制和增删改查过程。 1.使用心得&#xff1a; 1.1 当map为nil和map为空时&#xff0c;增删改查操作时会出现的不同情况 我们可以发现&#…

【全开源】废品回收微信小程序基于FastAdmin+ThinkPHP+UniApp

介绍 一款基于FastAdminThinkPHPUniApp开发的废品回收系统&#xff0c;适用废品回收站、再生资源回收公司上门回收使用的小程序 功能特性 1、会员注册 支持小程序授权注册和手机号注册 2、回收品类 可设置回收品类&#xff0c;废纸、废金属、废玻璃、旧衣服等 3、今日指导价…

面试高频知识点:Java互联网大厂高频面试题(持续收录)

文章目录 前言一、Java基础题1、Java语言的三大特性2、JDK 和 JRE 有什么区别3、Java基本数据类型及其封装类4、说明一下public static void main(String args[])这段声明里关键字的作用5、java的数据结构有哪些&#xff1f;6、抽象类和接口的区别?7、 与 equals 的区别8、Str…

WordPress插件Show IDs by Echo,后台显示文章、页面、分类、标签、媒体库、评论、用户的ID

WordPress的这款Show IDs by Echo插件&#xff0c;可以让我们设置是增加一列ID还是直接在“编辑 |快速编辑 |查看”操作后面增加ID&#xff0c;而且支持展示以下内容的ID&#xff1a; 文章页面类别标签评论自定义帖子类型自定义分类法用户媒体 Show IDs by Echo插件的安装及启…

企业级OV SSL证书:强化在线信任与安全的权威之选

在数字经济浪潮下&#xff0c;企业网站的安全性直接影响着用户信任度和业务的可持续发展。其中&#xff0c;企业级组织验证&#xff08;Organization Validation&#xff0c;简称OV&#xff09;SSL证书作为安全解决方案的重要一环&#xff0c;以其独有的优势&#xff0c;在众多…

网安面经之文件包含漏洞

一、文件包含漏洞 1、文件包含漏洞原理&#xff1f;危害&#xff1f;修复&#xff1f; 原理&#xff1a;开发⼈员⼀般希望代码更灵活&#xff0c;所以将被包含的⽂件设置为变量&#xff0c;⽤来进⾏动态调⽤&#xff0c;但是由于⽂件包含函数加载的参数没有经过过滤或者严格的…