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本身不直接支持,但可以根据业务逻辑设计自定义的锁选择策略)。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

整理好的中债国债3年期到期收益率数据集(2002-2023年)

01、数据简介 国债&#xff0c;又称国家公债&#xff0c;是由国家发行的债券&#xff0c;是中央ZF为筹集CZ资金而发行的一种ZF债券&#xff0c;是中央ZF向投资者出具的、承诺在一定时期支付利息和到期偿还本金的债权债务凭证。 中债&#xff0c;是指由中国中债登记结算有限责…

【ROS2】功能包

文章目录 ROS2 功能包创建功能包编译功能包设置环境变量功能包的结构C 功能包结构Python 功能包结构 参考链接 ROS2 功能包 在 ROS2 工作空间的 src 目录下进行编写的文件并不是普通的文件&#xff0c;而是被称作功能包。 创建功能包 Usage: ros2 pkg create --build-type …

【C/C++】内存分布

本文第一部分主要介绍了程序内存区域的划分以及数据的存储。第二部分有一段代码和一些题目&#xff0c;全面直观得分析了程序中的数组在内存中的存储。 因为不同的数据有不同的存储需求&#xff0c;各区域满足不同的需求&#xff0c;所以程序内存会有区域的划分。 根据需求的不…

线程同步--条件变量,信号量

生产者和消费者模型 案例 /*生产者消费者模型&#xff08;粗略的版本&#xff09; */ #include <stdio.h> #include <pthread.h> #include <stdlib.h> #include <unistd.h>// 创建一个互斥量 pthread_mutex_t mutex;struct Node{int num;struct Node …

练习题(2024/5/12)

1二分查找 给定一个 n 个元素有序的&#xff08;升序&#xff09;整型数组 nums 和一个目标值 target &#xff0c;写一个函数搜索 nums 中的 target&#xff0c;如果目标值存在返回下标&#xff0c;否则返回 -1。 示例 1: 输入: nums [-1,0,3,5,9,12], target 9 输出: 4…

异常处理/ROS2异常处理模块源码解读与浅析

文章目录 概述ros2/rcutils/src/error_handling模块自身异常处理错误状态结构与存储本模块初始化错误状态的设置错误状态的获取错误状态的清理不丢失旧错误状态把手段还原为目的其他 概述 本文从如下几个方面对 ROS2.0 中 rcutils 库 error_handling 错误处理模块的源码进行解…

day-34 二叉树的锯齿形层序遍历

思路 相较于二叉树的层序遍历&#xff0c;多了一个flag变量,当flag等于0时&#xff0c;把当前层的数组从左到右放入链表&#xff0c;当flag等于1时&#xff0c;把当前层的数组从右到左放入链表。 解题方法 注意&#xff1a;链表删除一个数据后会立即重排&#xff0c;所以删除同…

Linux进程控制——Linux进程终止

前言&#xff1a;前面了解完前面的Linux进程基础概念后&#xff0c;我们算是解决了Linux进程中的一大麻烦&#xff0c;现在我们准备更深入的了解Linux进程——Linux进程控制&#xff01; 我们主要介绍的Linux进程控制内容包括&#xff1a;进程终止&#xff0c;进程等待与替换&a…

GoF之代理模式(静态代理+动态代理(JDK动态代理+CGLIB动态代理带有一步一步详细步骤))

1. GoF之代理模式&#xff08;静态代理动态代理(JDK动态代理CGLIB动态代理带有一步一步详细步骤)&#xff09; 文章目录 1. GoF之代理模式&#xff08;静态代理动态代理(JDK动态代理CGLIB动态代理带有一步一步详细步骤)&#xff09;每博一文案2. 代理模式的理解3. 静态代理4. 动…

函数式接口-闭包与柯里化

闭包 定义 示例 注意 这个外部变量 x 必须是effective final 你可以生命他是final&#xff0c;你不声明也会默认他是final的&#xff0c;并且具有final的特性&#xff0c;不可变一旦x可变&#xff0c;他就不是final&#xff0c;就无法形成闭包&#xff0c;也无法与函数对象一…

单链表经典算法OJ题---力扣206,876(带图详解

1.链接&#xff1a;. - 力扣&#xff08;LeetCode&#xff09;【点击即可跳转】 思路&#xff1a;创建三个指针&#xff0c;看下图 注意&#xff1a;n3如果为空&#xff0c;则不能继续指向下一节点&#xff0c;需要进行判断 代码实现&#xff1a; struct ListNode* reverseLi…

手游掘金最新玩法,单条视频变现1w+,一部手机即可操作,保姆级教程

如果你也想通过手机赚钱&#xff0c;在这里有一个非常好的项目&#xff0c;它可以让你轻松赚到额外的收入。 这个手游掘金最新玩法&#xff0c;是一个非常受欢迎的项目&#xff0c;它可以让你通过制作单条视频来获得高额收益。不同于传统的游戏赚钱方式&#xff0c;这个方法不…

树莓派安装opencv

安装opencv 上述步骤完成后&#xff0c;输入以下代码(基于python3) sudo apt-get install python3-opencv -y不行的话&#xff0c;试试换源&#xff0c;然后 sudo apt-get update成功&#xff01; 测试opencv是否安装成功 输入 python3 然后再输入 import cv2 没有报错就…

闲来装个虚拟机Ubuntu24.04和硬盘分区及挂载

简述 最近ubuntu出新版本了&#xff0c;ubuntu24.04&#xff0c; 俗称高贵食蚁兽。5年前进行Android或者linux开发基本是在windows下的虚拟机中进行。目前&#xff0c;虽然物质基础提高了&#xff0c;功能有独立进行编译、代码管理的服务器了。可以通过ssh登录&#xff0c;但是…

[Bug]:由于中国防火墙,无法连接 huggingface.co

问题描述 : OSError: We couldnt connect to https://huggingface.co to load this file, couldnt find it in the cached files and it looks like youscan/ukr-roberta-base is not the path to a directory containing a file named config. Json. Checkout your internet …

【数据结构】排序(一)—— 希尔排序(思路演进版)

目录 一、常见的排序算法分类 二、常见排序算法的实现 2.1插入排序 2.1.1基本思想 2.1.2直接插入排序 思路 step1.单趟控制 step2.总体控制 代码实现 测试 特性总结 2.1.3 希尔排序( 缩小增量排序 ) 基本思想 思路演进 &#x1f308;1.代码实现单组排序&#…

GD32用ST-Link出现internal command error的原因及解决方法

一、GD32 F407烧录时出现can not reset target shutting down debug session 搜寻网上资料&#xff0c;发现解决方式多种多样&#xff0c;做一个简单的总结&#xff1a; 1.工程路径包含中文名 2.需更改debug选项 3.引脚冲突 4.杜邦线太长 而先前我的工程路径包含中文名也仍…

Java Array 数组

文章目录 Java Array 数组一&#xff0c;数组的介绍1. 数组的理解(Array)2. 数组相关的概念3. 数组的特点:4. 变量按照数据类型的分类5. 数组的分类6. 一维数组的使用(6个基本点)7. 数组元素的默认初始化值的情况 二&#xff0c;一维数组1. 一维数组功能测试2. 一维数组案例(1)…

香港虚拟主机哪里可以试用?用于企业建站的

香港虚拟主机适合个人、企业建站&#xff0c;包括外贸企业网站、个人博客网站、中小企业官网等&#xff0c;那么作为新手不知道哪家香港虚拟主机好用的时候&#xff0c;该如何找到可以试用的香港虚拟主机呢&#xff1f; 香港虚拟主机也称作香港空间、香港虚拟空间&#xff0c;…

深入探索Android应用数据共享之ContentProvider

本文将深入探讨Android开发中非常重要的数据共享机制 - ContentProvider。 主要内容包括: ContentProvider的基本定义及特点如何实现一个自定义的ContentProviderContentProvider对外提供的功能以及对外部应用的权限控制对ContentProvider的一些常见使用场景使用ContentProvi…