ReentrantReadWriteLock理解

ReentrantLock在并发情况下只允许单个线程执行受保护的代码,而在大部分应用中都是读多写少,所以,如果使用ReentrantLock实现这种对共享数据的并发访问控制,将严重影响整体的性能。ReentrantReadWriteLock中提供的读取锁(ReadLock)可以实现并发访问下的多读,写入锁(WriteLock)可以实现每次只允许一个写操作。

ReentrantLock有两个锁:一个是与读相关的锁,称为“共享锁“;另一个是与写相关的锁,称为”排他锁“。也就是多个读锁之间不互斥,读锁与写锁互斥,写锁与写锁互斥。

示例代码-读读:

public class ReentrantReadWriteLockTest {
private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

public static void main(String[] args) {
ReentrantReadWriteLockTest reentrantReadWriteLockTest = new ReentrantReadWriteLockTest();
new Thread(reentrantReadWriteLockTest::read,"ThreadA").start();
new Thread(reentrantReadWriteLockTest::read,"ThreadB").start();

}

private void read() {
lock.readLock().lock();
System.out.println("获得读锁" + Thread.currentThread().getName() + "时间" + System.currentTimeMillis());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.readLock().unlock();
}
}
}

 


可以看到两个线程几乎是同时获取锁,说明readLock.lock允许多个线程同时执行lock()后面代码的方法。

写写示例

private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

public static void main(String[] args) {
ReentrantReadWriteLockTest reentrantReadWriteLockTest = new ReentrantReadWriteLockTest();
new Thread(reentrantReadWriteLockTest::write, "ThreadA").start();
new Thread(reentrantReadWriteLockTest::write, "ThreadB").start();

}

private void write() {
lock.writeLock().lock();
System.out.println("获得读锁" + Thread.currentThread().getName() + "时间" + System.currentTimeMillis());
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.writeLock().unlock();
}
}

 


可以看出差了大致5秒的时间,多个写线程之间是互斥的。

读写或写读示例

private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

public static void main(String[] args) {
ReentrantReadWriteLockTest reentrantReadWriteLockTest = new ReentrantReadWriteLockTest();
new Thread(reentrantReadWriteLockTest::read, "ThreadA").start();
new Thread(reentrantReadWriteLockTest::write, "ThreadB").start();

}


private void read() {
lock.readLock().lock();
System.out.println("获得读锁" + Thread.currentThread().getName() + "时间" + System.currentTimeMillis());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.readLock().unlock();
}
}

private void write() {
lock.writeLock().lock();
System.out.println("获得读锁" + Thread.currentThread().getName() + "时间" + System.currentTimeMillis());
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.writeLock().unlock();
}
}

 


可以看出时间大致差了3秒,可以说明读写线程是互斥的。

下面我们来探究一下这具体的原理:
读写锁主要具有以下特性:

公平性选择:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平;
重进入:该锁支持重进入,以读写线程为例:读线程在获取了读锁之后,能够再次获取读锁。而写线程在获取了写锁之后能够再次获取写锁,同时也可以获取读锁;
锁降级:遵循获取写锁、获取读锁在释放写锁的次序,写锁能够降级成为读锁;
锁获取中断:读取锁和写入锁都支持获取锁期间被中断. 这个和独占锁一致;
支持条件变量:写入锁提供了条件变量(Condition)的支持, 这个和独占锁一致, 但是读取锁却不允许获取条件变量, 将得到一个UnsupportedOperationException异常。

 


ReentrantReadWriteLock实现了接口ReadWriteLock,该接口提供了两个方法,一个用于获取读锁,另一个用于获取写锁。

Lock readLock();

Lock writeLock();

 


它提供了和ReentrantLock类似的公平锁和非公平锁(默认构造方法是非公平锁),Sync类是一个继承于AQS的抽象类。Sync有FairSync公平锁和NonfairSync非公平锁两个子类:

public ReentrantReadWriteLock() {
this(false);
}


public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}

 


ReentrantReadWriteLock中包含了下面三个对象:sync对象,读锁readerLock和写锁writerLock。读锁ReadLock和写锁WriteLock都实现了Lock接口。读锁ReadLock和写锁WriteLock中也都分别包含了"Sync对象",它们的Sync对象和ReentrantReadWriteLock的Sync对象 是一样的,就是通过sync,读锁和写锁实现了对同一个对象的访问。

ReentrantReadWriteLock实现的ReadWriteLock接口方法为;

public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }

 


可以看到,读写锁内部有两个类,分别为写锁WriteLock、读锁ReadLock:

public static class WriteLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -4992448646407690164L;
private final Sync sync;

protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}

public void lock() {
sync.acquire(1);
}

public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}

public boolean tryLock( ) {
return sync.tryWriteLock();
}

public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

public void unlock() {
sync.release(1);
}

public Condition newCondition() {
return sync.newCondition();
}
...
}

public static class ReadLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -5992448646407690164L;
private final Sync sync;

protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}

public void lock() {
sync.acquireShared(1);
}

public void lockInterruptibly() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}

public boolean tryLock() {
return sync.tryReadLock();
}

public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

public void unlock() {
sync.releaseShared(1);
}

public Condition newCondition() {
throw new UnsupportedOperationException();
}

public String toString() {
int r = sync.getReadLockCount();
return super.toString() +
"[Read locks = " + r + "]";
}
}

 

这两个类都实现了Lock接口,从源码中可以看出,读锁、写锁的操作都是依靠Sync类来实现,而Sync是一个抽象类,具体由NonfaitSync和FairSync类来实现。下面是Sync类的主要属性和方法:

static final int SHARED_SHIFT = 16;//读锁同步状态占用的位数
//每次增加读锁同步状态,就相当于增加SHARED_UNIT
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
//读锁或写或的最大请求数量
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
//低16位的MASK,用来计算写锁的同步状态
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;

//返回共享写数
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
//返回独占锁数
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

 

在ReentrantLock自定义同步器的实现中,同步状态表示锁被一个线程重复获取的次数,而读写锁的自定义同步器需要在同步状态(一个整形变量)上维护多个读线程和一个写线程的状态,那就需要“按位切割”使用这个状态变量,读写锁将变量切分成两部分,高16位表示读,低16位表示写。

 

 

 

当前同步状态表示一个线程已经获取了写锁,且重进入了2次,同时也连续获取了两次读锁。同步状态是通过位运算进行更新的,假设当前同步状态是S,写状态等于S & EXCLUSIVE_MASK,即S & 0x0000FFFF,读状态等于S >>> 16.当写状态加1时,等于S+1,当读状态加1时,等于S+SHARED_UNIT,即S+(1 << 16),也就是S + 0x00010000。

即读锁和写锁的状态获取和设置如下:

读锁状态的获取:S >> 16
读锁状态的增加:S + (1 << 16)
写锁状态的获取:S & 0x0000FFFF
写锁状态的增加:S + 1
写锁就是一个支持可重入的排他锁

 

写锁的获取:

protected final boolean tryAcquire(int acquires) {
//获取当前线程对象
Thread current = Thread.currentThread();
//获取同步状态
int c = getState();
//获取写状态
int w = exclusiveCount(c);
//同步状态不为0,至少有一个读或写锁被线程获取
if (c != 0) {
//写状态为0,读状态不为0;或者写状态不为0,读状态为0,且获取写锁的线程不是当前线程
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// 写状态不为0,读状态为0,且获取写锁的线程是当前线程,判断写锁的获取次数是不是要超出限制
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 重入获取写锁,修改同步状态
setState(c + acquires);
return true;
}

// 是否需要阻塞,该方法由子类实现,或者同步状态修改失败
if (writerShouldBlock() ||!compareAndSetState(c, c + acquires))
return false;


// 不需要阻塞且同步状态修改成功,设置获取独占锁的线程为当前线程
setExclusiveOwnerThread(current);
return true;
}

 

该方法和ReentrantLock的tryAcquire(int)方法大致一样,只不过在判断重入时增加了一个读锁是否存在的判断。因为要确保写锁的操作对读锁是可见的,如果在读锁存在的情况下允许获取写锁,那么那些已经获取读锁的其他线程可能就无法感知当前写线程的操作。因此只有等读锁完全释放后,写锁才能够被当前线程所获取,一旦写锁获取了,所有其他读、写线程均会被阻塞。

写锁的释放
写锁的释放最终会调用Sync类的tryRelease(int)方法:

protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}

 


写锁的释放与ReentrantLock的释放过程基本类似,每次释放均是减少写状态,当写状态为0时表示写锁已经完全释放了,从而等待的读写线程能够继续访问读写锁,获取同步状态,同时此次写线程的修改对后续的写线程可见。

读锁的获取

public void lock() {
//调用AQS的acquireShared(int)方法
sync.acquireShared(1);
}

public final void acquireShared(int arg) {
//tryAcquireShared为抽象方法,为具体子类实现
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}

protected final int tryAcquireShared(int unused) {
//获取当前线程
Thread current = Thread.currentThread();
//获取同步状态
int c = getState();
// 如果存在写锁,且持有写锁的线程不是当前对象,返回-1,表示获取读锁失败
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
//获取读锁
int r = sharedCount(c);

// 如果读锁不需要等待且读锁获取次数没超过限制,同时同步状态修改成功
if (!readerShouldBlock() && r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
// 读锁第一次被线程获取
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
// 读锁被第一次获取读锁的线程重复获取
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}

// 获取读锁失败,调用fullTryAcquireShared(Thread)方法,放到循环里重试
return fullTryAcquireShared(current);
}

 

读锁的释放,最终会调到Sync重写的tryReleaseShared(int)方法

protected final boolean tryReleaseShared(int unused) {
// 获取当前线程对象
Thread current = Thread.currentThread();
// 如果想要释放锁的线程为第一个获取锁的线程
if (firstReader == current) {
// 当前线程仅获取了一次读锁,则需要将firstReader设置null,否则firstReaderHoldCount - 1
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
// 通过CAS操作更新同步状态
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}

 

HoldCounter
HoldCounter保存了线程持有共享锁(读锁)的数量,包括重入的数量,HoldCounter类主要起着计数器的作用,对读锁的获取与释放操作会更新对应的计数值。若线程获取读锁,则该计数器+1,释放读锁,该计数器-1。只有当线程获取读锁后才能对读锁进行释放、重入操作。

static final class HoldCounter {
// 计数器
int count = 0;
// 线程ID
final long tid = getThreadId(Thread.currentThread());
}

 


HoldCounter类很简单,只有一个计数器count变量和线程ID tid变量,在Java中,若是我们需要将某个对象与线程绑定,就只有ThreadLocal类才能实现了。在ReentrantReadWriteLock类中还有一个ThreadLocal类的子类:

static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}

 


通过上面的类HoldCounter就可以与线程进行绑定了。从上面我们可以看到ThreadLocal将HoldCounter绑定到当前线程上,同时HoldCounter也持有线程ID,这样在释放锁的时候才能知道ReadWriteLock里面缓存的上一个读取线程(cachedHoldCounter)是否是当前线程。这样做的好处是可以减少ThreadLocal.get()的次数,因为这也是一个耗时操作。需要说明的是这样HoldCounter绑定线程ID而不绑定线程对象的原因是避免HoldCounter和ThreadLocal互相绑定而GC难以释放它们(尽管GC能够智能的发现这种引用而回收它们,但是这需要一定的代价),所以其实这样做只是为了帮助GC快速回收对象而已。

 

本文摘自,如有侵权,请联系我!!!

 

posted @ 2022-03-09 13:40  高压锅里的大萝卜  阅读(61)  评论(0编辑  收藏  举报