ReentrantReadWriteLock

news/2024/10/22 14:37:57/

关于读写锁状态的存取

// ReentrantReadWriteLock.Sync
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;/** Returns the number of shared holds represented in count  */
static int sharedCount(int c)    { return c >>> SHARED_SHIFT; }// 高16位存储读锁
/** Returns the number of exclusive holds represented in count  */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }// 低16位存储写锁
// 设置读锁的状态
compareAndSetState(c, c + SHARED_UNIT)
// 设置写锁的状态
compareAndSetState(c, c + 1)

使用state(int类型,32位)的高16位存储读锁状态,低16位存储写锁状态

读写锁的结构

ReentrantReadWriteLock本身不是一把锁,但是它管理着两把锁,读锁和写锁。写锁是独占锁,读锁是共享锁。

这里ReadWriteLock只是一个接口,并不是一把锁。

private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;

公平锁和非公平锁

static final class NonfairSync extends Sync {private static final long serialVersionUID = -8159625535654395037L;final boolean writerShouldBlock() {return false; // writers can always barge}final boolean readerShouldBlock() {// 返回false会尝试去获取锁return apparentlyFirstQueuedIsExclusive();}
}
// 这个方法指用在获取读锁上,即获取共享锁上
// 即下一个要被唤醒的节点是不是独占节点,即这个节点是不是在申请写锁
final boolean apparentlyFirstQueuedIsExclusive() {Node h, s;// 下一个要唤醒的节点不是共享节点,就返回true// 即如果下一个要唤醒的是申请写锁的线程,就阻塞读锁的获取return (h = head) != null &&(s = h.next)  != null &&!s.isShared()         &&s.thread != null;
}static final class FairSync extends Sync {private static final long serialVersionUID = -2274990926593161451L;final boolean writerShouldBlock() {return hasQueuedPredecessors();}final boolean readerShouldBlock() {return hasQueuedPredecessors();}
}public final boolean hasQueuedPredecessors() {Node t = tail; // Read fields in reverse initialization orderNode h = head;Node s;// h!=t证明起码有一个线程在等待 s.thread!= currentThread 因为读,写锁都是可重入的,所以,如果等待的这个线程正好是当前线程// 可以直接放行让它获取锁return h != t &&((s = h.next) == null || s.thread != Thread.currentThread());
}

在ReentrantLock中,公平锁与非公平锁的区别是,非公平锁不管同步队列中有没有线程在排队,都会先去获取锁。而公平锁,就是直接先去排队。

那么同样的定义,在ReentrantReadWriteLock中也体现得很明显。

FairSync和NonFairSync都重写了writerShouldBlock,readerShouldBlock方法。

writerShouldBlock用在获取独占锁,也就是写锁的方法上。

// Sync  acquire方法会调用tryAcquire方法获取锁,acquire方法会被WriteLock.lock方法调用,即获取写锁的时候调用
protected final boolean tryAcquire(int acquires) {Thread current = Thread.currentThread();int c = getState();int w = exclusiveCount(c);if (c != 0) {// (Note: if c != 0 and w == 0 then shared count != 0)if (w == 0 || current != getExclusiveOwnerThread())// c != 0,w == 0,证明有线程获取了读锁return false;if (w + exclusiveCount(acquires) > MAX_COUNT)throw new Error("Maximum lock count exceeded");// Reentrant acquiresetState(c + acquires);return true;}// 如果是公平锁,writerShouldBlock将会看前面有没有线程在排队,有排队的,writerShouldBlock才返回true,即这里直接返回false(获取锁失败)// 如果是非公平锁,这里返回false,会调用CAS设置state,设置失败才返回false// 所以,就一个writerShouldBlock方法就可以实现了公平锁和非公平锁if (writerShouldBlock() ||!compareAndSetState(c, c + acquires))return false;setExclusiveOwnerThread(current);return true;
}

获取写锁流程图如下:

readerShouldBlock方法被tryAcquireShared和fullyTryAcquireShared调用

// Sync tryAcquireShared会被acquireShared调用,acquireShared被ReadLock.lock调用,即获取读锁的时候调用
protected final int tryAcquireShared(int unused) {Thread current = Thread.currentThread();int c = getState();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;}return fullTryAcquireShared(current);
}

 读锁和写锁

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();// 这是唯一一个没有用AQS方法的方法}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();}
}
// ReentrantReadWriteLock.Sync
final boolean tryReadLock() {Thread current = Thread.currentThread();for (;;) {int c = getState();// exclusiveCount指的是获取写锁的数目// 如果有别的线程已经获取了写锁,则失败(如果是同一个线程获取了写锁,则无所谓,因为同一个线程的读写肯定不会冲突)if (exclusiveCount(c) != 0 &&getExclusiveOwnerThread() != current)return false;int r = sharedCount(c);// 读锁的数目if (r == MAX_COUNT)throw new Error("Maximum lock count exceeded");if (compareAndSetState(c, c + SHARED_UNIT)) {// 获取读锁成功if (r == 0) {// 如果原来没有线程获取到读锁firstReader = current;firstReaderHoldCount = 1;} else if (firstReader == current) {firstReaderHoldCount++;} else {// 一个同步器有一个cachedHoldCounter,一个Lock有一个同步器// cachedHoldCounter是上一个获取到读锁的线程的获取锁的数目// 如果当前线程获取了读锁,更新占有锁的数目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 true;}}
}

可以看到ReadLock中,除了tryLock方法,其他调的都是AQS提供的方法。其中lock方法也在公平锁和非公平锁中讲过了。

tryLock的流程也跟lock的流程基本一致,除了lock中加了readerShouldBlock方法的判断。这里就不详解了。

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 boolean isHeldByCurrentThread() {return sync.isHeldExclusively();}public int getHoldCount() {return sync.getWriteHoldCount();}
}
// ReentrantReadWriteLock.Sync
final boolean tryWriteLock() {Thread current = Thread.currentThread();int c = getState();if (c != 0) {// 可能有写锁或读锁被获取了int w = exclusiveCount(c);// w == 0表示,如果有线程获取了读锁,就直接失败吗(为什么)不是同一个线程可以直接获取读锁和写锁吗?如果它之前已经获取了读锁,现在再来获取写锁会直接失败吗?// 或者有线程获取了写锁,但是只要不是当前线程,就直接失败if (w == 0 || current != getExclusiveOwnerThread())return false;if (w == MAX_COUNT)throw new Error("Maximum lock count exceeded");}if (!compareAndSetState(c, c + 1))return false;setExclusiveOwnerThread(current);return true;
}

写锁与读锁结构也基本一致,自己实现了tryLock方法,流程与tryAcquire基本一致,这里不细讲。

读写锁的同步队列

读写锁虽然持有了两把锁,但是只有一个Sync,即只有一个同步队列。

那只有一个同步队列,是怎么管理独占节点和共享节点的阻塞和唤醒的呢?

获取锁在上面都讲了,现在讲讲读写锁是怎么释放的。

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;
}

这是写锁的释放,总体来说比较简单。

看看当前线程还持有多少个锁,如果释放完以后,还持有0个锁,就将当前独占的线程设为空。

protected final boolean tryReleaseShared(int unused) {Thread current = Thread.currentThread();if (firstReader == current) {// assert firstReaderHoldCount > 0;if (firstReaderHoldCount == 1)firstReader = null;elsefirstReaderHoldCount--;} 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;}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;}
}

实例

构建几个场景:

场景一:

读+读+写(这里读、写分别代表申请读写的线程,按+的先后顺序,表示线程申请读写锁的时间顺序,比如在这里就代表,线程1申请了读锁,之后,线程2申请读锁,线程3申请写锁),这时候同步队列应该是什么样的呢?

public class LockExample {public void testReadWriteLock() {ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();CountDownLatch countDownLatch = new CountDownLatch(3);t1ReadLock(readWriteLock,countDownLatch,0,3);t2WriteLock(readWriteLock,countDownLatch,2,3);t3ReadLock(readWriteLock,countDownLatch,1,3);try {countDownLatch.await();} catch (InterruptedException e) {}}
}

以下为线程对应的工作,在所有场景中会复用。

 private void t1ReadLock(ReentrantReadWriteLock readWriteLock,CountDownLatch countDownLatch,int startDelay, int duration) {Thread thread1 = new Thread(new Runnable() {@Overridepublic void run() {try {TimeUnit.SECONDS.sleep(startDelay);} catch (InterruptedException e) {}System.out.println("thread1 begin fetch readLock");readWriteLock.readLock().lock();System.out.println("thread1 has fetch readLock");try {System.out.println("======= thread1 do things ==========");TimeUnit.SECONDS.sleep(duration);// 休眠过程中不会放开锁} catch (InterruptedException e) {} finally {System.out.println("now readWriteLock queue:"+readWriteLock.getQueueLength());System.out.println("======= thread1 do things end ,unlock readLock ==========");readWriteLock.readLock().unlock();countDownLatch.countDown();}}});thread1.start();}private void t2WriteLock(ReentrantReadWriteLock readWriteLock,CountDownLatch countDownLatch,int startDelay,int duration) {Thread thread2 = new Thread(new Runnable() {@Overridepublic void run() {try {TimeUnit.SECONDS.sleep(startDelay);} catch (InterruptedException e) {}System.out.println("thread2 begin fetch writeLock");readWriteLock.writeLock().lock();try {System.out.println("======= thread2 do things ==========");TimeUnit.SECONDS.sleep(duration);} catch (InterruptedException e) {} finally {System.out.println("now readWriteLock queue:"+readWriteLock.getQueueLength());System.out.println("======= thread2 do things end,unlock writeLock ==========");readWriteLock.writeLock().unlock();countDownLatch.countDown();}}});thread2.start();}private void t3ReadLock(ReentrantReadWriteLock readWriteLock,CountDownLatch countDownLatch,int startDelay, int duration) {Thread thread3 = new Thread(new Runnable() {@Overridepublic void run() {try {TimeUnit.SECONDS.sleep(startDelay);} catch (InterruptedException e) {}System.out.println("thread3 begin fetch readLock");readWriteLock.readLock().lock();System.out.println("thread3 has fetch readLock");try {System.out.println("======= thread3 do things ==========");TimeUnit.SECONDS.sleep(duration);} catch (InterruptedException e) {} finally {System.out.println("now readWriteLock queue:"+readWriteLock.getQueueLength());System.out.println("======= thread3 do things end unLock ReadLock ==========");readWriteLock.readLock().unlock();countDownLatch.countDown();}}});thread3.start();}

thread1 begin fetch readLock

thread1 has fetch readLock

======= thread1 do things ==========

thread3 begin fetch readLock

thread3 has fetch readLock

======= thread3 do things ==========

thread2 begin fetch writeLock

now readWriteLock queue:1

======= thread1 do things end ,unlock readLock ==========

now readWriteLock queue:1

======= thread3 do things end unLock ReadLock ==========

======= thread2 do things ==========

now readWriteLock queue:0

======= thread2 do things end,unlock writeLock ==========

从结果可以看出,读+读+写的时候,两个读锁可以并行执行,写锁需要等两个读锁执行完后才能获取锁。看同步队列中,线程数目为1也可以证实这一点。

从代码逻辑来看:

获取写锁的前提是,当前没有任何线程持有读锁,且写锁的获取者为当前线程,或没有线程获取到写锁。

获取读锁的前提是,除了当前线程,其他线程都没有获取到写锁。

所以读+读+写,最终的结论是(读,读+写)

场景二:

读+写+读

public void testReadWriteLock() {ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();CountDownLatch countDownLatch = new CountDownLatch(3);t1ReadLock(readWriteLock,countDownLatch,0,3);t2WriteLock(readWriteLock,countDownLatch,1,3);// 只调整了写锁和读锁的开始时间t3ReadLock(readWriteLock,countDownLatch,2,3);try {countDownLatch.await();} catch (InterruptedException e) {}
}

thread1 begin fetch readLock

thread1 has fetch readLock

======= thread1 do things ==========

thread2 begin fetch writeLock

thread3 begin fetch readLock

now readWriteLock queue:2

======= thread1 do things end ,unlock readLock ==========

======= thread2 do things ==========

now readWriteLock queue:1

======= thread2 do things end,unlock writeLock ==========

thread3 has fetch readLock

======= thread3 do things ==========

now readWriteLock queue:0

======= thread3 do things end unLock ReadLock ==========

 

神奇了!如果按场景一的理解,这时候的结果就应该跟场景一的一致。但现在线程3却跟线程2一起阻塞了,这是为什么呢?

原因是,读写锁默认是非公平锁。

非公平锁中重写了readerShouldBlock方法。其中readerShouldBlock方法中

判断,如果下一个要唤醒的节点为写锁,则阻塞当前读锁的获取(至于为什么这么做,可能是因为担心饥饿问题把,毕竟读锁是共享的,如果一直都让读锁共享,很容易导致写锁一直没有机会获取)。

所以,对于读+写+读,最终的顺序就是(读+写+读)

 

附+测试:

对于读+写+读+读,后面两个读线程会并发执行吗?

public void testReadWriteLock() {final int DURATION_TIME = 5;ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();CountDownLatch countDownLatch = new CountDownLatch(4);t1ReadLock(readWriteLock,countDownLatch,0,DURATION_TIME);t2WriteLock(readWriteLock,countDownLatch,1,DURATION_TIME);t3ReadLock(readWriteLock,countDownLatch,2,DURATION_TIME);t4ReadLock(readWriteLock,countDownLatch,3,DURATION_TIME);try {countDownLatch.await();} catch (InterruptedException e) {}
}

thread1 begin fetch readLock

thread1 has fetch readLock

======= thread1 do things ==========

thread2 begin fetch writeLock

thread3 begin fetch readLock

thread4 begin fetch readLock

now readWriteLock queue:3

======= thread1 do things end ,unlock readLock ==========

======= thread2 do things ==========

now readWriteLock queue:2

======= thread2 do things end,unlock writeLock ==========

thread3 has fetch readLock

======= thread3 do things ==========

thread4 has fetch readLock

======= thread4 do things ==========

now readWriteLock queue:0

now readWriteLock queue:0

======= thread3 do things end unLock ReadLock ==========

======= thread4 do things end unLock ReadLock ==========

 

这里后面thread3和thread4是并行执行完的。因为线程2释放的时候,会去释放线程3。线程3在执行doAcquireShared方法的时候,会释放后继节点,即线程4。

场景三:

写+写+读

这其实很简单,写线程是独占锁,所以最终顺序就是写+写+读

public void testReadWriteLock() {final int DURATION_TIME = 4;ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();CountDownLatch countDownLatch = new CountDownLatch(3);t1ReadLock(readWriteLock,countDownLatch,2,DURATION_TIME);t2WriteLock(readWriteLock,countDownLatch,0,DURATION_TIME);t5WriteLock(readWriteLock,countDownLatch,1,DURATION_TIME);try {countDownLatch.await();} catch (InterruptedException e) {}
}

thread2 begin fetch writeLock

======= thread2 do things ==========

thread5 begin fetch writeLock

thread1 begin fetch readLock

now readWriteLock queue:2

======= thread2 do things end,unlock writeLock ==========

======= thread5 do things ==========

now readWriteLock queue:1

======= thread5 do things end,unlock writeLock ==========

thread1 has fetch readLock

======= thread1 do things ==========

now readWriteLock queue:0

======= thread1 do things end ,unlock readLock ==========

 

场景四:

同一个线程,先获取写锁再获取读锁

private void writeToReadLock(ReentrantReadWriteLock readWriteLock) {Thread thread = new Thread(()->{readWriteLock.writeLock().lock();System.out.println("do write things");readWriteLock.readLock().lock();try {} finally {System.out.println("unlock writeLock");readWriteLock.writeLock().unlock();System.out.println("unlock readLock");readWriteLock.readLock().unlock();}});thread.start();
}


http://www.ppmy.cn/news/1033728.html

相关文章

MYSQL-习题掌握

文章目录 SQL基本操作1 设计表操作1.1 关系表字段1.2 关系表创建1.3 关系表数据1.4 关系表关系 2 SQL操作2.1 SQL 1-102.2 SQL 11-202.3 SQL 21-302.4 SQL 31-402.5 SQL 41-50 SQL基本操作 1 设计表操作 1.1 关系表字段 1 学生表 student s_ids_names_births_sex学生编号学…

库存管理系统哪个好?亿发云南省大型智能WMS仓储信息解决方案,免费更新

在当今激烈的仓储物流竞争中&#xff0c;企业的成功关键在于加强作业效率和降低话费。随着科技的不断更新&#xff0c;传统仓储方式已逐渐被智能仓储管理系统&#xff08;WMS&#xff09;取代&#xff0c;并逐渐成为行业趋势。大数据时代下&#xff0c;引进行之有效的智能管理系…

轻薄的ESL电子标签有哪些特性?

在智慧物联逐渐走进千万家的当下&#xff0c;技术变革更加日新月异。ESL电子标签作为科技物联的重要组成部分&#xff0c;是推动千行百业数字化转型的重要技术&#xff0c;促进物联网产业的蓬勃发展。在智慧零售、智慧办公、智慧仓储等领域&#xff0c;ESL电子标签在未来是不可…

TypeScript教程(四)基本运算符

一、运算符 TypeScript包含以下几种运算符&#xff1a; 1.算术运算符 2.逻辑运算符 3.关系运算符 4.按位运算符 5.赋值运算符 6.三元/条件运算符 7.字符串运算符 8.类型运算符 1.算术运算符 y5 运算符描述例子x 运算结果y 运算结果加法xy275-减法xy-235*乘法xy*2105…

el-table分页后序号连续的两种方法

实现效果&#xff1a; 第一页排序到10&#xff0c;第二页的排序应从11开始 实现方法一&#xff1a; 在el-table的序号列中使用template定义 <el-table><el-table-columnmin-width"10%"label"序号"><template slot-scope"scope"…

Scratch 之 制作超丝滑 FNF 推条

这个教程是不用画笔的&#xff0c;所以不用担心推条是最后一层了&#xff01; 导入素材 你以为真是这样吗&#xff1f;NO&#xff0c;NO&#xff0c;NO&#xff0c;其实是这样的 没错&#xff0c;中间是空的&#xff01;中间是空的&#xff01;中间是空的&#xff01;&#xf…

bigemap如何添加mapbox地图?

第一步 打开浏览器&#xff0c;找到你要访问的地图的URL地址&#xff0c;并且确认可以正常在浏览器中访问&#xff1b;浏览器中不能访问&#xff0c;同样也不能在软件中访问。 以下为常用地图源地址&#xff1a; 天地图&#xff1a; http://map.tianditu.gov.cn 包含&…

C++设计模式结构型之代理模式

一、概述 代理模式是一种结构型模式&#xff0c;在很多不同的场合具有广泛的分类和应用。其主要实现的思想是在客户端和真正要访问的对象之间引入一个 代理对象&#xff08;间接层&#xff09;&#xff0c;于是&#xff0c;以往客户端对真正对象的访问现在变成了通过代理对…