简介
ReentrantReadWriteLock 是 Java 并发包(java.util.concurrent.locks)中的一个类,它实现了一个可重入的读写锁。读写锁允许多个线程同时读取共享资源,但在写入共享资源时只允许一个线程进行。这种锁机制特别适用于读多写少的场景,可以显著提高并发性能。
ReentrantReadWriteLock 包含两个锁:一个用于读操作(ReadLock),另一个用于写操作(WriteLock)。这两个锁是互斥的,即一个线程拥有写锁时,其他线程无法获取读锁或写锁;一个线程拥有读锁时,其他线程无法获取写锁,但可以获取读锁(这取决于锁的公平性和当前读锁的持有数量)。
以下是 ReentrantReadWriteLock 的一些关键特性和用法:
- 可重入性:持有锁的线程可以再次获取同一个锁,而不会被阻塞。
- 公平性:可以通过构造函数指定锁是否公平。公平锁会按照线程请求锁的顺序来授予锁,而非公平锁则不保证这种顺序。
- 读锁和写锁:通过 readLock() 和 writeLock() 方法获取读锁和写锁。
- 锁升级:一个线程可以先获取读锁,然后升级为写锁,但不允许先获取写锁再降级为读锁。
源码
public class ReentrantReadWriteLockimplements ReadWriteLock, java.io.Serializable {private static final long serialVersionUID = -6992448646407690164L;/** 读锁*/private final ReentrantReadWriteLock.ReadLock readerLock;/**写锁 */private final ReentrantReadWriteLock.WriteLock writerLock;/** Performs all synchronization mechanics */final Sync sync;/*** 默认构造是非公平锁*/public ReentrantReadWriteLock() {this(false);}
}
HoldCounter
// 计数器
//HoldCounter主要有两个属性,count和tid,其中count表示某个读线程重入的次数,tid表示该线程的tid字段的值,该字段可以用来唯一标识一个线程。
static final class HoldCounter {// 计数int count = 0;// Use id, not reference, to avoid garbage retention// 获取当前线程的TID属性的值final long tid = getThreadId(Thread.currentThread());
}
ThreadLocalHoldCounter
//ThreadLocalHoldCounter重写了ThreadLocal的initialValue方法,ThreadLocal类可以将线程与对象相关联。在没有进行set的情况下,get到的均是initialValue方法里面生成的那个HolderCounter对象。
static final class ThreadLocalHoldCounterextends ThreadLocal<HoldCounter> {public HoldCounter initialValue() {return new HoldCounter();}
}
Sync
abstract static class Sync extends AbstractQueuedSynchronizer {// 版本序列号private static final long serialVersionUID = 6317671515068378041L; // 高16位为读锁,低16位为写锁static final int SHARED_SHIFT = 16;// 读锁单位static final int SHARED_UNIT = (1 << SHARED_SHIFT);// 读锁最大数量static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;// 写锁最大数量static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;// 本地线程计数器private transient ThreadLocalHoldCounter readHolds;// 缓存的计数器private transient HoldCounter cachedHoldCounter;// 第一个读线程private transient Thread firstReader = null;// 第一个读线程的计数private transient int firstReaderHoldCount;
}
tryAcquire()
protected final boolean tryAcquire(int acquires) {//当前线程Thread current = Thread.currentThread();//获取状态int c = getState();//写线程数量(即获取独占锁的重入数)int w = exclusiveCount(c);//当前同步状态state != 0,说明已经有其他线程获取了读锁或写锁if (c != 0) {// 当前state不为0,此时:如果写锁状态为0说明读锁此时被占用返回false;// 如果写锁状态不为0且写锁没有被当前线程持有返回falseif (w == 0 || current != getExclusiveOwnerThread())return false;//判断同一线程获取写锁是否超过最大次数(65535),支持可重入if (w + exclusiveCount(acquires) > MAX_COUNT)throw new Error("Maximum lock count exceeded");//更新状态//此时当前线程已持有写锁,现在是重入,所以只需要修改锁的数量即可。setState(c + acquires);return true;}//到这里说明此时c=0,读锁和写锁都没有被获取//writerShouldBlock表示是否阻塞if (writerShouldBlock() ||!compareAndSetState(c, c + acquires))return false;//设置锁为当前线程所有setExclusiveOwnerThread(current);return true;
}
(1)首先获取c、w。c表示当前锁状态;w表示写线程数量。然后判断同步状态state是否为0。如果state!=0,说明已经有其他线程获取了读锁或写锁,执行(2);否则执行(5)。
(2)如果锁状态不为零(c != 0),而写锁的状态为0(w = 0),说明读锁此时被其他线程占用,所以当前线程不能获取写锁,自然返回false。或者锁状态不为零,而写锁的状态也不为0,但是获取写锁的线程不是当前线程,则当前线程也不能获取写锁。
(3)判断当前线程获取写锁是否超过最大次数,若超过,抛异常,反之更新同步状态(此时当前线程已获取写锁,更新是线程安全的),返回true。
(4)如果state为0,此时读锁或写锁都没有被获取,判断是否需要阻塞(公平和非公平方式实现不同),在非公平策略下总是不会被阻塞,在公平策略下会进行判断(判断同步队列中是否有等待时间更长的线程,若存在,则需要被阻塞,否则,无需阻塞),如果不需要阻塞,则CAS更新同步状态,若CAS成功则返回true,失败则说明锁被别的线程抢去了,返回false。如果需要阻塞则也返回false。
(5)成功获取写锁后,将当前线程设置为占有写锁的线程,返回true。
tryRelease()
protected final boolean tryRelease(int releases) {//若锁的持有者不是当前线程,抛出异常if (!isHeldExclusively())throw new IllegalMonitorStateException();//写锁的新线程数int nextc = getState() - releases;//如果独占模式重入数为0了,说明独占模式被释放boolean free = exclusiveCount(nextc) == 0;if (free)//若写锁的新线程数为0,则将锁的持有者设置为nullsetExclusiveOwnerThread(null);//设置写锁的新线程数//不管独占模式是否被释放,更新独占重入数setState(nextc);return free;
}
写锁的释放过程还是相对而言比较简单的:首先查看当前线程是否为写锁的持有者,如果不是抛出异常。然后检查释放后写锁的线程数是否为0,如果为0则表示写锁空闲了,释放锁资源将锁的持有线程设置为null,否则释放仅仅只是一次重入锁而已,并不能将写锁的线程清空。
说明:此方法用于释放写锁资源,首先会判断该线程是否为独占线程,若不为独占线程,则抛出异常,否则,计算释放资源后的写锁的数量,若为0,表示成功释放,资源不将被占用,否则,表示资源还被占用。
tryAcquireShared()
protected final int tryAcquireShared(int unused) {// 获取当前线程Thread current = Thread.currentThread();// 获取状态int c = getState();//如果写锁线程数 != 0 ,且独占锁不是当前线程则返回失败,因为存在锁降级if (exclusiveCount(c) != 0 &&getExclusiveOwnerThread() != current)return -1;// 读锁数量int r = sharedCount(c);/** readerShouldBlock():读锁是否需要等待(公平锁原则)* r < MAX_COUNT:持有线程小于最大数(65535)* compareAndSetState(c, c + SHARED_UNIT):设置读取锁状态*/// 读线程是否应该被阻塞、并且小于最大值、并且比较设置成功if (!readerShouldBlock() &&r < MAX_COUNT &&compareAndSetState(c, c + SHARED_UNIT)) {//r == 0,表示第一个读锁线程,第一个读锁firstRead是不会加入到readHolds中if (r == 0) { // 读锁数量为0// 设置第一个读线程firstReader = current;// 读线程占用的资源数为1firstReaderHoldCount = 1;} else if (firstReader == current) { // 当前线程为第一个读线程,表示第一个读锁线程重入// 占用资源数加1firstReaderHoldCount++;} else { // 读锁数量不为0并且不为当前线程// 获取计数器HoldCounter rh = cachedHoldCounter;// 计数器为空或者计数器的tid不为当前正在运行的线程的tidif (rh == null || rh.tid != getThreadId(current))// 获取当前线程对应的计数器cachedHoldCounter = rh = readHolds.get();else if (rh.count == 0) // 计数为0//加入到readHolds中readHolds.set(rh);//计数+1rh.count++;}return 1;}return fullTryAcquireShared(current);
}
读锁获取锁的过程比写锁稍微复杂些,首先判断写锁是否为0并且当前线程不占有独占锁,直接返回;否则,判断读线程是否需要被阻塞并且读锁数量是否小于最大值并且比较设置状态成功,若当前没有读锁,则设置第一个读线程firstReader和firstReaderHoldCount;若当前线程线程为第一个读线程,则增加firstReaderHoldCount;否则,将设置当前线程对应的HoldCounter对象的值。
tryReleaseShared()
protected final boolean tryReleaseShared(int unused) {// 获取当前线程Thread current = Thread.currentThread();if (firstReader == current) { // 当前线程为第一个读线程// assert firstReaderHoldCount > 0;if (firstReaderHoldCount == 1) // 读线程占用的资源数为1firstReader = null;else // 减少占用的资源firstReaderHoldCount--;} else { // 当前线程不为第一个读线程// 获取缓存的计数器HoldCounter rh = cachedHoldCounter;if (rh == null || rh.tid != getThreadId(current)) // 计数器为空或者计数器的tid不为当前正在运行的线程的tid// 获取当前线程对应的计数器rh = readHolds.get();// 获取计数int count = rh.count;if (count <= 1) { // 计数小于等于1// 移除readHolds.remove();if (count <= 0) // 计数小于等于0,抛出异常throw unmatchedUnlockException();}// 减少计数--rh.count;}for (;;) { // 无限循环// 获取状态int c = getState();// 获取状态int nextc = c - SHARED_UNIT;if (compareAndSetState(c, nextc)) // 比较并进行设置// Releasing the read lock has no effect on readers,// but it may allow waiting writers to proceed if// both read and write locks are now free.return nextc == 0;}
}
此方法表示读锁线程释放锁。首先判断当前线程是否为第一个读线程firstReader,若是,则判断第一个读线程占有的资源数firstReaderHoldCount是否为1,若是,则设置第一个读线程firstReader为空,否则,将第一个读线程占有的资源数firstReaderHoldCount减1;若当前线程不是第一个读线程,那么首先会获取缓存计数器(上一个读锁线程对应的计数器 ),若计数器为空或者tid不等于当前线程的tid值,则获取当前线程的计数器,如果计数器的计数count小于等于1,则移除当前线程对应的计数器,如果计数器的计数count小于等于0,则抛出异常,之后再减少计数即可。无论何种情况,都会进入无限循环,该循环可以确保成功设置状态state。
一个线程要想同时持有写锁和读锁,必须先获取写锁再获取读锁;写锁可以“降级”为读锁;读锁不能“升级”为写锁。
示例
import java.util.concurrent.locks.ReentrantReadWriteLock; public class SharedResource { private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); private int data; public void read() { rwLock.readLock().lock(); // 获取读锁 try { // 执行读操作,如读取 data 变量 System.out.println("Read: " + data); } finally { rwLock.readLock().unlock(); // 释放读锁 } } public void write(int newData) { rwLock.writeLock().lock(); // 获取写锁 try { // 执行写操作,如修改 data 变量 data = newData; System.out.println("Write: " + data); } finally { rwLock.writeLock().unlock(); // 释放写锁 } }
}
在这个示例中,SharedResource 类包含一个整数 data 和一个 ReentrantReadWriteLock。read() 方法使用读锁来读取 data,而 write(int newData) 方法使用写锁来修改 data。通过这种方式,多个线程可以同时读取 data,但在写入 data 时,只有一个线程可以执行此操作。