深入解析ReentrantLock与StampedLock的使用技巧

2024-05-05 00:52

本文主要是介绍深入解析ReentrantLock与StampedLock的使用技巧,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

ReentrantLock

1. 概要介绍

1.1 ReentrantLock 背景和定义

在多线程并发编程中,锁是一种基础且关键的同步机制,它帮助我们协调不同线程之间对共享资源的访问,确保数据的一致性和完整性。ReentrantLock,即“可重入锁”,是 java.util.concurrent.locks 包中的一个类,它实现了 Lock 接口并提供了与 synchronized 关键字相似的同步功能。与 synchronized 相比,ReentrantLock 提供了更灵活的锁定操作,并支持更丰富的功能。

import java.util.concurrent.locks.ReentrantLock;public class ReentrantLockExample {private final ReentrantLock lock = new ReentrantLock();public void performLocking() {lock.lock();try {// 保护的临界区代码} finally {lock.unlock();}}
}

1.2 ReentrantLock 和 synchronized 的比较

1.2.1 基本差异

synchronized 是 Java 的内置关键字,提供了对对象进行原子性操作的能力;而 ReentrantLock 是一个 Java 类,需要通过显式的锁定(lock())和解锁(unlock())方法来实现同步。不仅如此,ReentrantLock 还提供了尝试非阻塞地获取锁(tryLock())、可中断的锁获取(lockInterruptibly())以及实现公平锁等高级功能。

1.2.2 性能对比

在单一锁竞争较少的场景中,两者性能差别不大。但是在高并发、锁竞争激烈的情况下,ReentrantLock 的性能通常优于 synchronized,因为 ReentrantLock 提供了尝试锁、定时锁等高级功能,这些功能使开发者能够更精细地控制锁的行为,从而在一些复杂的同步场景下有更高的性能。

1.2.3 场景适应性

synchronized 由于其简单性,非常适合那些代码结构简单、同步需求不高的场景。而 ReentrantLock 则适用于更复杂的并发场景,如需要公平性、可中断、条件锁等高级同步特性时。

2. ReentrantLock 的高级功能

2.1 公平锁与非公平锁

ReentrantLock 允许创建公平锁或非公平锁:公平锁意味着在多个线程竞争的情况下,锁的分配将遵循 FIFO 规则;非公平锁则是在竞争时不考虑等待时间,可能会存在“插队”的情况。通常情况下,非公平锁的性能高于公平锁,因为后者要维护一个有序队列。

2.2 条件变量(Conditions)

条件变量可以用于更细粒度的线程协调。ReentrantLock 与条件变量 Condition 结合使用时,可以让线程在某些条件不满足时暂停执行(通过 Condition.await() 方法),直到另外一个线程改变条件并通知 Condition(通过 Condition.signal() 或 Condition.signalAll() 方法)。

2.3 可中断的锁获取

ReentrantLock 提供了 lockInterruptibly() 方法,允许在等待锁的过程中响应中断。这是 synchronized 所不支持的功能,它可以帮助你处理死锁或长时间等待锁的问题,在合适的时候安全地终止线程的执行。

2.4 锁的粗细化与优化

ReentrantLock 让开发者有机会进行更精细的锁管理。通过分离多个锁,我们可以仅在需要保护的资源上加锁,这样可以减少竞争并提高效率。

3. ReentrantLock 的实践应用

3.1 实现一个线程安全的计数器

计数器是并发程序中最简单的共享资源之一。使用 ReentrantLock 可以确保在多线程环境下更新计数器的安全性。

public class Counter {private int count = 0;private final ReentrantLock lock = new ReentrantLock();public void increment() {lock.lock();try {count++;} finally {lock.unlock();}}public int getCount() {return count;}
}

3.2 构建一个简单的阻塞队列

阻塞队列是多线程编程中的一个常用组件,用于线程间的数据交换和协调。下列代码示例展示了如何利用 ReentrantLock 和条件变量 Condition 来构建一个线程安全的阻塞队列。

import java.util.LinkedList;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;public class BlockingQueue<T> {private final LinkedList<T> queue = new LinkedList<>();private final int capacity;private final ReentrantLock lock = new ReentrantLock();private final Condition notFull = lock.newCondition();private final Condition notEmpty = lock.newCondition();public BlockingQueue(int capacity) {this.capacity = capacity;}public void put(T element) throws InterruptedException {lock.lock();try {while (queue.size() == capacity) {notFull.await();}queue.add(element);notEmpty.signalAll();} finally {lock.unlock();}}public T take() throws InterruptedException {lock.lock();try {while (queue.size() == 0) {notEmpty.await();}T item = queue.removeFirst();notFull.signalAll();return item;} finally {lock.unlock();}}
}

3.3 实现多条件的生产者消费者问题

生产者-消费者问题是一个典型的同步问题。通过使用 ReentrantLock 的条件变量 Condition,我们可以在同一个锁上创建多个条件队列,分别为生产者和消费者提供等待队列,如下所示。

public class ProducerConsumerExample {private LinkedList<Integer> buffer = new LinkedList<>();private int maxSize = 10;private ReentrantLock lock = new ReentrantLock();private Condition full = lock.newCondition();private Condition empty = lock.newCondition();class Producer implements Runnable {@Overridepublic void run() {while (true) {lock.lock();try {while (buffer.size() == maxSize) {full.await();}buffer.add((int) (Math.random() * 1000));empty.signalAll();} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}}}class Consumer implements Runnable {@Overridepublic void run() {while (true) {lock.lock();try {while (buffer.isEmpty()) {empty.await();}int value = buffer.poll();System.out.println("Consumed: " + value);full.signalAll();} catch (InterruptedException e) {e.printStackTrace();} finally {lock.unlock();}}}}
}

在这个示例中,我们有一个共享的 buffer,通过 lock 保证线程安全。Producer 在 buffer 满时等待,Consumer 在 buffer 空时等待。使用 full.await() 和 empty.await() 来挂起线程,full.signalAll() 和 empty.signalAll() 来唤醒等待的线程。

ReentrantReadWriteLock

1. 概要介绍

1.1 ReentrantReadWriteLock 的作用和设计初衷

ReentrantReadWriteLock是一种读写锁,它允许多个线程同时读取一个资源而不会发生冲突,但是如果有线程想要写入资源,则必须独占访问权。这种锁适用于读操作远多于写操作的场景,因为它可以提高程序的性能和吞吐量。

1.2 读写锁的工作原理

读写锁维护了一对锁,一个读锁和一个写锁。当没有线程持有写锁时,多个线程可以同时获得读锁。但是一旦有线程请求了写锁,其他线程就无法获得读锁或写锁,保证了写入时的独占访问。

2. ReentrantReadWriteLock 的实践应用

2.1 缓存系统中的使用案例

在缓存系统中,数据的读取次数往往远远大于更新次数。使用 ReentrantReadWriteLock 可以提高缓存系统读取数据时的并发性能。

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;public class CacheWithReadWriteLock {private final Map<String, Object> cacheMap = new HashMap<>();private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();public Object get(String key) {readLock.lock();try {return cacheMap.get(key);} finally {readLock.unlock();}}public Object put(String key, Object value) {writeLock.lock();try {return cacheMap.put(key, value);} finally {writeLock.unlock();}}// ...其他方法
}

2.2 读多写少场景中的性能优化

在档案系统或配置中心等读多写少的场景中,ReentrantReadWriteLock 通过允许并发读取,可以极大地提高这类系统的性能。

2.3 实现线程安全的数据结构,如Map

下面的示例代码展示了如何使用 ReentrantReadWriteLock 实现线程安全的 Map:

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;public class ThreadSafeMap<K, V> {private final Map<K, V> map = new HashMap<>();private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();public V get(K key) {readLock.lock();try {return map.get(key);} finally {readLock.unlock();}}public void put(K key, V value) {writeLock.lock();try {map.put(key, value);} finally {writeLock.unlock();}}// ...其他方法
}

在这个例子中,对 Map 的读取操作使用了读锁,更新操作使用了写锁。这样可以保证在更新数据时,所有的读操作都会等待,直到写操作完成。

StampedLock

1. 概要介绍

1.1 StampedLock 的设计与特点

StampedLock 是在 Java 8 中引入的,它提供了一种乐观读锁的实现,这可以在没有写入时增加程序的并发度。与 ReentrantReadWriteLock 不同,StampedLock 的锁定方法会返回一个标记(stamp)以表示锁的状态。

1.2 与 ReadWriteLock 的对比

StampedLock 支持三种模式的锁:写锁、悲观读锁和乐观读。乐观读是 StampedLock 与 ReadWriteLock 最大的不同之处。乐观读允许完全无锁的访问,只在数据完整性上检查有无冲突,提供了一种无锁的读取方式,这通常用于数据结构的维护中。

2. StampedLock 的实践应用

2.1 StampedLock 在几何计算中的应用

在几何计算应用中,读取数据的操作远多于写入操作。下面是一个使用 StampedLock 管理几何形状数据结构的例子:

import java.util.concurrent.locks.StampedLock;public class Point {private double x, y;private final StampedLock sl = new StampedLock();void move(double deltaX, double deltaY) {long stamp = sl.writeLock();try {x += deltaX;y += deltaY;} finally {sl.unlockWrite(stamp);}}double distanceFromOrigin() {long stamp = sl.tryOptimisticRead();double currentX = x, currentY = y;if (!sl.validate(stamp)) {stamp = sl.readLock();try {currentX = x;currentY = y;} finally {sl.unlockRead(stamp);}}return Math.sqrt(currentX * currentX + currentY * currentY);}// ...其他方法
}

在这个例子中,move() 方法使用写锁来改变点的位置,而 distanceFromOrigin() 方法首先尝试一个乐观读,然后验证返回的标记,如果标记无效(说明在读取数据的同时有其他线程进行了写操作),则升级到悲观读锁以确保数据的一致性。

2.2 实现乐观读取的数据结构

乐观读取通常用于数据很少修改的情景,以下是利用 StampedLock 实现的一个线程安全并且支持乐观读取的数据结构例子。

import java.util.concurrent.locks.StampedLock;public class OptimisticReadExample {private volatile int value = 0;private final StampedLock lock = new StampedLock();public void update(int newValue) {long stamp = lock.writeLock();try {value = newValue;} finally {lock.unlockWrite(stamp);}}public int read() {long stamp = lock.tryOptimisticRead();int readValue = value;// 验证乐观读后,数据是否被其他线程更改if (!lock.validate(stamp)) {// 乐观读失败,升级为悲观读锁stamp = lock.readLock();try {readValue = value;} finally {lock.unlockRead(stamp);}}return readValue;}
}

在上述代码中,update() 方法通过写锁来保证数据更新的原子性。而 read() 方法首先尝试乐观读取,通过验证 stamp 来确认在读取过程中数据是否被修改,如果被修改,则通过获取悲观读锁来保证数据读取的一致性。

Condition

1. 概要介绍

1.1 Condition 接口的概念与用途

Condition接口提供了一种在特定Lock对象上等待的手段,这比传统的对象监视方法(wait、notify和notifyAll)提供了更丰富的线程控制手段。一个Lock对象可以绑定多个Condition对象,它们可以控制线程的暂停(await())和唤醒(signal()/signalAll())。

1.2 Condition 与 Object 监视器方法的比较

Condition相比于传统的监视器方法,其优势在于支持多个等待集,即可以有多个线程等待条件的队列,这在某些算法和数据结构设计中更为高效和灵活。

2. Condition 的实践应用

2.1 使用Condition实现有界队列

Condition可以用于有界队列的实现,它可以协助处理队列的空和满的状态。下面是一个使用ReentrantLock和Condition实现的有界队列示例:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;public class BoundedQueue<T> {private Object[] items;// 添加的下标,删除的下标和当前数量private int addIndex, removeIndex, count;private ReentrantLock lock = new ReentrantLock();private Condition notEmpty = lock.newCondition();private Condition notFull = lock.newCondition();public BoundedQueue(int size) {items = new Object[size];}public void add(T t) throws InterruptedException {lock.lock();try {while (count == items.length) {notFull.await();}items[addIndex] = t;if (++addIndex == items.length) {addIndex = 0;}++count;notEmpty.signal();} finally {lock.unlock();}}@SuppressWarnings("unchecked")public T remove() throws InterruptedException {lock.lock();try {while (count == 0) {notEmpty.await();}Object x = items[removeIndex];if (++removeIndex == items.length) {removeIndex = 0;}--count;notFull.signal();return (T)x;} finally {lock.unlock();}}
}

2.2 在ReentrantLock中使用多个Condition实现精细化管理线程

在复杂的同步场景中,可能需要多种条件来控制线程的状态。例如,在一个生产者-消费者模型中,notFull和notEmpty两个条件可以被分别用来控制生产者和消费者的行为。

2.3 结合Condition和StampedLock实现复杂的同步机制

虽然StampedLock不支持条件变量,但我们可以结合Condition和StampedLock来解决一些更复杂的同步场景。这通常意味着需要额外的同步机制,如使用ReentrantLock来实现Condition,同时使用StampedLock来提供乐观读功能。

这篇关于深入解析ReentrantLock与StampedLock的使用技巧的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/960468

相关文章

gitlab安装及邮箱配置和常用使用方式

《gitlab安装及邮箱配置和常用使用方式》:本文主要介绍gitlab安装及邮箱配置和常用使用方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1.安装GitLab2.配置GitLab邮件服务3.GitLab的账号注册邮箱验证及其分组4.gitlab分支和标签的

SpringBoot3应用中集成和使用Spring Retry的实践记录

《SpringBoot3应用中集成和使用SpringRetry的实践记录》SpringRetry为SpringBoot3提供重试机制,支持注解和编程式两种方式,可配置重试策略与监听器,适用于临时性故... 目录1. 简介2. 环境准备3. 使用方式3.1 注解方式 基础使用自定义重试策略失败恢复机制注意事项

nginx启动命令和默认配置文件的使用

《nginx启动命令和默认配置文件的使用》:本文主要介绍nginx启动命令和默认配置文件的使用,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录常见命令nginx.conf配置文件location匹配规则图片服务器总结常见命令# 默认配置文件启动./nginx

在Windows上使用qemu安装ubuntu24.04服务器的详细指南

《在Windows上使用qemu安装ubuntu24.04服务器的详细指南》本文介绍了在Windows上使用QEMU安装Ubuntu24.04的全流程:安装QEMU、准备ISO镜像、创建虚拟磁盘、配置... 目录1. 安装QEMU环境2. 准备Ubuntu 24.04镜像3. 启动QEMU安装Ubuntu4

使用Python和OpenCV库实现实时颜色识别系统

《使用Python和OpenCV库实现实时颜色识别系统》:本文主要介绍使用Python和OpenCV库实现的实时颜色识别系统,这个系统能够通过摄像头捕捉视频流,并在视频中指定区域内识别主要颜色(红... 目录一、引言二、系统概述三、代码解析1. 导入库2. 颜色识别函数3. 主程序循环四、HSV色彩空间详解

Windows下C++使用SQLitede的操作过程

《Windows下C++使用SQLitede的操作过程》本文介绍了Windows下C++使用SQLite的安装配置、CppSQLite库封装优势、核心功能(如数据库连接、事务管理)、跨平台支持及性能优... 目录Windows下C++使用SQLite1、安装2、代码示例CppSQLite:C++轻松操作SQ

一文深入详解Python的secrets模块

《一文深入详解Python的secrets模块》在构建涉及用户身份认证、权限管理、加密通信等系统时,开发者最不能忽视的一个问题就是“安全性”,Python在3.6版本中引入了专门面向安全用途的secr... 目录引言一、背景与动机:为什么需要 secrets 模块?二、secrets 模块的核心功能1. 基

qt5cored.dll报错怎么解决? 电脑qt5cored.dll文件丢失修复技巧

《qt5cored.dll报错怎么解决?电脑qt5cored.dll文件丢失修复技巧》在进行软件安装或运行程序时,有时会遇到由于找不到qt5core.dll,无法继续执行代码,这个问题可能是由于该文... 遇到qt5cored.dll文件错误时,可能会导致基于 Qt 开发的应用程序无法正常运行或启动。这种错

python常见环境管理工具超全解析

《python常见环境管理工具超全解析》在Python开发中,管理多个项目及其依赖项通常是一个挑战,下面:本文主要介绍python常见环境管理工具的相关资料,文中通过代码介绍的非常详细,需要的朋友... 目录1. conda2. pip3. uvuv 工具自动创建和管理环境的特点4. setup.py5.

Python常用命令提示符使用方法详解

《Python常用命令提示符使用方法详解》在学习python的过程中,我们需要用到命令提示符(CMD)进行环境的配置,:本文主要介绍Python常用命令提示符使用方法的相关资料,文中通过代码介绍的... 目录一、python环境基础命令【Windows】1、检查Python是否安装2、 查看Python的安