深入解析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

相关文章

Java 正则表达式URL 匹配与源码全解析

《Java正则表达式URL匹配与源码全解析》在Web应用开发中,我们经常需要对URL进行格式验证,今天我们结合Java的Pattern和Matcher类,深入理解正则表达式在实际应用中... 目录1.正则表达式分解:2. 添加域名匹配 (2)3. 添加路径和查询参数匹配 (3) 4. 最终优化版本5.设计思

Java使用ANTLR4对Lua脚本语法校验详解

《Java使用ANTLR4对Lua脚本语法校验详解》ANTLR是一个强大的解析器生成器,用于读取、处理、执行或翻译结构化文本或二进制文件,下面就跟随小编一起看看Java如何使用ANTLR4对Lua脚本... 目录什么是ANTLR?第一个例子ANTLR4 的工作流程Lua脚本语法校验准备一个Lua Gramm

Java字符串操作技巧之语法、示例与应用场景分析

《Java字符串操作技巧之语法、示例与应用场景分析》在Java算法题和日常开发中,字符串处理是必备的核心技能,本文全面梳理Java中字符串的常用操作语法,结合代码示例、应用场景和避坑指南,可快速掌握字... 目录引言1. 基础操作1.1 创建字符串1.2 获取长度1.3 访问字符2. 字符串处理2.1 子字

Java Optional的使用技巧与最佳实践

《JavaOptional的使用技巧与最佳实践》在Java中,Optional是用于优雅处理null的容器类,其核心目标是显式提醒开发者处理空值场景,避免NullPointerExce... 目录一、Optional 的核心用途二、使用技巧与最佳实践三、常见误区与反模式四、替代方案与扩展五、总结在 Java

使用Java将DOCX文档解析为Markdown文档的代码实现

《使用Java将DOCX文档解析为Markdown文档的代码实现》在现代文档处理中,Markdown(MD)因其简洁的语法和良好的可读性,逐渐成为开发者、技术写作者和内容创作者的首选格式,然而,许多文... 目录引言1. 工具和库介绍2. 安装依赖库3. 使用Apache POI解析DOCX文档4. 将解析

Qt中QUndoView控件的具体使用

《Qt中QUndoView控件的具体使用》QUndoView是Qt框架中用于可视化显示QUndoStack内容的控件,本文主要介绍了Qt中QUndoView控件的具体使用,具有一定的参考价值,感兴趣的... 目录引言一、QUndoView 的用途二、工作原理三、 如何与 QUnDOStack 配合使用四、自

Java字符串处理全解析(String、StringBuilder与StringBuffer)

《Java字符串处理全解析(String、StringBuilder与StringBuffer)》:本文主要介绍Java字符串处理全解析(String、StringBuilder与StringBu... 目录Java字符串处理全解析:String、StringBuilder与StringBuffer一、St

C++使用printf语句实现进制转换的示例代码

《C++使用printf语句实现进制转换的示例代码》在C语言中,printf函数可以直接实现部分进制转换功能,通过格式说明符(formatspecifier)快速输出不同进制的数值,下面给大家分享C+... 目录一、printf 原生支持的进制转换1. 十进制、八进制、十六进制转换2. 显示进制前缀3. 指

Spring Boot循环依赖原理、解决方案与最佳实践(全解析)

《SpringBoot循环依赖原理、解决方案与最佳实践(全解析)》循环依赖指两个或多个Bean相互直接或间接引用,形成闭环依赖关系,:本文主要介绍SpringBoot循环依赖原理、解决方案与最... 目录一、循环依赖的本质与危害1.1 什么是循环依赖?1.2 核心危害二、Spring的三级缓存机制2.1 三

C#中async await异步关键字用法和异步的底层原理全解析

《C#中asyncawait异步关键字用法和异步的底层原理全解析》:本文主要介绍C#中asyncawait异步关键字用法和异步的底层原理全解析,本文给大家介绍的非常详细,对大家的学习或工作具有一... 目录C#异步编程一、异步编程基础二、异步方法的工作原理三、代码示例四、编译后的底层实现五、总结C#异步编程