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

相关文章

使用EasyExcel实现简单的Excel表格解析操作

《使用EasyExcel实现简单的Excel表格解析操作》:本文主要介绍如何使用EasyExcel完成简单的表格解析操作,同时实现了大量数据情况下数据的分次批量入库,并记录每条数据入库的状态,感兴... 目录前言固定模板及表数据格式的解析实现Excel模板内容对应的实体类实现AnalysisEventLis

使用国内镜像源优化pip install下载的方法步骤

《使用国内镜像源优化pipinstall下载的方法步骤》在Python开发中,pip是一个不可或缺的工具,用于安装和管理Python包,然而,由于默认的PyPI服务器位于国外,国内用户在安装依赖时可... 目录引言1. 为什么需要国内镜像源?2. 常用的国内镜像源3. 临时使用国内镜像源4. 永久配置国内镜

Go语言中最便捷的http请求包resty的使用详解

《Go语言中最便捷的http请求包resty的使用详解》go语言虽然自身就有net/http包,但是说实话用起来没那么好用,resty包是go语言中一个非常受欢迎的http请求处理包,下面我们一起来学... 目录安装一、一个简单的get二、带查询参数三、设置请求头、body四、设置表单数据五、处理响应六、超

如何使用C#串口通讯实现数据的发送和接收

《如何使用C#串口通讯实现数据的发送和接收》本文详细介绍了如何使用C#实现基于串口通讯的数据发送和接收,通过SerialPort类,我们可以轻松实现串口通讯,并结合事件机制实现数据的传递和处理,感兴趣... 目录1. 概述2. 关键技术点2.1 SerialPort类2.2 异步接收数据2.3 数据解析2.

详解如何使用Python提取视频文件中的音频

《详解如何使用Python提取视频文件中的音频》在多媒体处理中,有时我们需要从视频文件中提取音频,本文为大家整理了几种使用Python编程语言提取视频文件中的音频的方法,大家可以根据需要进行选择... 目录引言代码部分方法扩展引言在多媒体处理中,有时我们需要从视频文件中提取音频,以便进一步处理或分析。本文

电脑报错cxcore100.dll丢失怎么办? 多种免费修复缺失的cxcore100.dll文件的技巧

《电脑报错cxcore100.dll丢失怎么办?多种免费修复缺失的cxcore100.dll文件的技巧》你是否也遇到过“由于找不到cxcore100.dll,无法继续执行代码,重新安装程序可能会解... 当电脑报错“cxcore100.dll未找到”时,这通常意味着系统无法找到或加载这编程个必要的动态链接库

使用Dify访问mysql数据库详细代码示例

《使用Dify访问mysql数据库详细代码示例》:本文主要介绍使用Dify访问mysql数据库的相关资料,并详细讲解了如何在本地搭建数据库访问服务,使用ngrok暴露到公网,并创建知识库、数据库访... 1、在本地搭建数据库访问的服务,并使用ngrok暴露到公网。#sql_tools.pyfrom

使用mvn deploy命令上传jar包的实现

《使用mvndeploy命令上传jar包的实现》本文介绍了使用mvndeploy:deploy-file命令将本地仓库中的JAR包重新发布到Maven私服,文中通过示例代码介绍的非常详细,对大家的学... 目录一、背景二、环境三、配置nexus上传账号四、执行deploy命令上传包1. 首先需要把本地仓中要

Spring Cloud之注册中心Nacos的使用详解

《SpringCloud之注册中心Nacos的使用详解》本文介绍SpringCloudAlibaba中的Nacos组件,对比了Nacos与Eureka的区别,展示了如何在项目中引入SpringClo... 目录Naacos服务注册/服务发现引⼊Spring Cloud Alibaba依赖引入Naco编程s依

Java springBoot初步使用websocket的代码示例

《JavaspringBoot初步使用websocket的代码示例》:本文主要介绍JavaspringBoot初步使用websocket的相关资料,WebSocket是一种实现实时双向通信的协... 目录一、什么是websocket二、依赖坐标地址1.springBoot父级依赖2.springBoot依赖