本文主要是介绍深入解析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的使用技巧的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!