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

news2024/11/28 20:33:48

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.coloradmin.cn/o/1668315.html

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

相关文章

17 SPI FLASH读写

SPI 协议简介 SPI 即 Serial Periphera linterface 的缩写&#xff0c;顾名思义就是串行外围设备接口&#xff0c;主要用于与FLASH、实时时钟、AD 转换器等外设模块的通信&#xff0c;它是一种高速的全双工同步的通信总线。 SPI 设备分为主设备和从设备&#xff0c;SPI 通信必…

vs code中如何使用git

由于本地代码有了一些储备&#xff0c;所以想通过网址托管形式&#xff0c;之前一直使用了github&#xff0c;但是鉴于一直被墙&#xff0c;无法登录账号&#xff0c;所以选择了国内的gitee来作为托管网站。 gitee的网址&#xff1a;Gitee - 基于 Git 的代码托管和研发协作平台…

蓝桥杯-网络安全比赛(7)基础知识 HTTP、TTL、IP数据包、MSS、MTU、ARP、LLMNR、MDNS、NBNS。

1. IP中TTL值能够给我提供什么信息&#xff1f;2. IP头部中标志、13位偏移、32位源IP地址、目标IP、IP数据包格式&#xff0c;有多少字节3. IP头部中的16位标识是什么&#xff1f;4. MSS 和MTU分别有多大&#xff1f;5. 怎么获取路由IP信息&#xff1f;PING、NSLOOKUP、TRACERT…

记忆化搜索专题

前言 如果要记忆化搜索的话&#xff0c;如果数据是10的九次方&#xff0c;我们不可能开一个那么大的数组来存储&#xff0c;所以我们要学会用map来存储 leecode1553 class Solution {unordered_map<int, int> memo; public:int minDays(int n) {if (n < 1) {return n;…

渗透测试-信息收集

网络安全信息收集是网络安全领域中至关重要的一环&#xff0c;它涉及到对目标系统、网络或应用进行全面而细致的信息搜集和分析。这一过程不仅有助于理解目标网络的结构、配置和潜在的安全风险&#xff0c;还能为后续的渗透测试、风险评估和安全加固提供有力的支持。 在网络安…

linux_用户与组

用户与组 基于账号的访问控制 账号类型&#xff1a;用户账号(UID) 、组账号(GID) 用户账号简介 作用: 1.可以登陆操作系统 2.不同的用户具备不同的权限 唯一标识&#xff1a;UID&#xff08;编号从0开始的编号&#xff0c;默认最大60000&#xff09; 管理员root的UID&…

鸿蒙开发接口Ability框架:【(AbilityContext)】

AbilityContext AbilityContext是Ability的上下文环境&#xff0c;继承自Context。 AbilityContext模块提供允许访问特定于ability的资源的能力&#xff0c;包括对Ability的启动、停止的设置、获取caller通信接口、拉起弹窗请求用户授权等。 说明&#xff1a; 本模块首批接口…

算法设计与分析 例题 绘制Huffman树、循环赛、分治、最短路与动态规划

1.考虑用哈夫曼算法来找字符a,b,c,d,e,f 的最优编码。这些字符出现在文件中 的频数之比为 20:10:6:4:44:16。要求&#xff1a; &#xff08;1&#xff09;&#xff08;4 分&#xff09;简述使用哈夫曼算法构造最优编码的基本步骤&#xff1b; &#xff08;2&#xff09;&…

代码已经推送到远程,如何退回之前的提交记录

现状&#xff1a;代码已经推送到远程&#xff0c;如何退回之前的提交记录 Sourcetree工具实现 最后使用命令强制推送&#xff1a; git push origin dev --forceidea 工具实现 强行推送

网络编程学习笔记1

文章目录 一、socket1、创建socket2、网络通信流程3、accept()函数4、signal()函数5、recv()函数6、connect()函数 二、I/O多路复用1.select模型2.poll模型3.epoll模型 注 一、socket 1、创建socket int socket(int domain,int type,int protocol); //返回值&#xff1a;一个…

ssm120基于SSM框架的金鱼销售平台的开发和实现+jsp

金鱼销售平台 摘 要 随着科学技术的飞速发展&#xff0c;各行各业都在努力与现代先进技术接轨&#xff0c;通过科技手段提高自身的优势&#xff1b;对于金鱼销售平台当然也不能排除在外&#xff0c;随着网络技术的不断成熟&#xff0c;带动了金鱼销售平台&#xff0c;它彻底改…

电商平台商品数据的价值在哪里?如何实现批量抓取?

一、电商平台商品数据的价值探秘 在数字经济的浪潮中&#xff0c;电商平台商品数据如同一座蕴藏着无尽宝藏的矿山&#xff0c;其价值远超过我们表面的认知。今天&#xff0c;就让我们一起揭开这座矿山的神秘面纱&#xff0c;探寻其中的奥秘。 首先&#xff0c;电商平台商品数…

FPGA第2篇,FPGA与CPU GPU APU DSP NPU TPU 之间的关系与区别

简介&#xff1a;首先&#xff0c;FPGA与CPU GPU APU NPU TPU DSP这些不同类型的处理器&#xff0c;可以被统称为"处理器"或者"加速器"。它们在计算机硬件系统中承担着核心的计算和处理任务&#xff0c;可以说是系统的"大脑"和"加速引擎&qu…

AI算法-高数5-线性代数1-基本概念、向量

线性代数&#xff1a;主要研究1、张量>CV计算机视觉 2、研究张量的线性关系。 深度学习的表现之所以能够超过传统的机器学习算法离不开神经网络&#xff0c;然而神经网络最基本的数据结构就是向量和矩阵&#xff0c;神经网络的输入是向量&#xff0c;然后通过每个矩阵对向量…

自动驾驶技术与传感器数据处理

目录 自动驾驶总体架构 感知系统 决策系统 定位系统 ​计算平台​ 仿真平台​ 自动驾驶公开数据集 激光点云 点云表征方式 1) 原始点云 2) 三维点云体素化 3)深度图 4)鸟瞰图 点云检测障碍物的步骤 PCL点云库 车载毫米波雷达 车载相机 设备标定 自动驾驶…

DMA原理、传输过程及传输方式

1.DMA DMA(Direct Memory Access&#xff0c;直接存储器访问)&#xff0c;是硬件实现存储器与存储器之间或存储器与I/O设备之间直接进行数据传输的内存技术&#xff0c;它允许不同速度的硬件设备(外设到内存、内存到外设、内存到内存、外设到外设)进行沟通&#xff0c;而不需要…

MySQL相关文件的介绍

其中的pid-file/var/run/mysqld/mysqld.pid是用来定义MySQL的进程ID的信息的&#xff0c; 这个ID是操作系统分配给MySQL服务进程的唯一标识&#xff0c;使得系统管理员可以轻松识别和管理该进程。 其中的log-error/var/log/mysqld.log是MySQL的错误日志文件&#xff0c;如果有…

C++string 类的常用方法

string (构造函数) (1) default 构造长度为零字符的空字符串。 (2) copy 构造 str 的副本。 (3) substring 复制从字符位置 pos 开始并跨越 len 字符的 str 部分&#xff08;如果任一 str 太短或 len 为 string&#xff1a;&#xff1a;npos&#xff0c;则复制 str 的末尾…

Ardupilot开源代码之Rover上路 - 后续1

Ardupilot开源代码之Rover上路 - 后续1 1. 源由2. 问题汇总2.1 问题1&#xff1a;飞控选择2.2 问题2&#xff1a;飞控安装位置和固定2.3 问题3&#xff1a;各种插头、插座配套2.4 问题4&#xff1a;分电板缺陷2.5 问题5&#xff1a;电机编码器接线及正反向问题2.6 问题6&#x…

docker-compose集成elk(基于logstash+filebeat)采集java和nginx日志

1.准备compose.yml编排式文件 services: #日志信息同步logstash:container_name: logstashimage: docker.elastic.co/logstash/logstash:7.17.14 #logstash:command: logstash -f /usr/share/logstash/pipeline/logstash.confdepends_on:- elasticsearchrestart: on-failurepo…