java并发排它锁_java并发-15-可重入读写锁

一、

介绍

之前提到锁(如Mutex和ReentrantLock)基本都是排他锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。

读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。除了保证写操作对读操作的可见性以及并发性的提升之外,读写锁能够简化读写交互场景的编程方式。假设在程序中定义一个共享的用作缓存数据结构,它大部分时间提供读服务(例如查询和搜索),而写操作占有的时间很少,但是写操作完成之后的更新需要对后续的读服务可见。

在没有读写锁支持的(Java

5之前)时候,如果需要完成上述工作就要使用Java的等待通知机制,就是当写操作开始时,所有晚于写操作的读操作均会进入等待状态,只有写操作完成并进行通知之后,所有等待的读操作才能继续执行(写操作之间依靠synchronized关键进行同步),这样做的目的是使读操作能读取到正确的数据,不会出现脏读。改用读写锁实现上述功能,只需要在读操作时获取读锁,写操作时获取写锁即可。

当写锁被获取到时,后续(非当前写操作线程)的读写操作都会被阻塞,写锁释放之后,所有操作继续执行,编程方式相对于使用等待通知机制的实现方式而言,变得简单明了。

一般情况下,读写锁的性能都会比排它锁好,因为大多数场景读是多于写的。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。Java并发包提供读写锁的实现是ReentrantReadWriteLock。

二、

ReentryReadWriteLock

ReentrantReadWriteLock内部还有静态类Sync继承了AQS,同时还包含了两个内部静态类WriteLock和ReadLock用于提供读锁和写锁。

1.

读写状态设计

读写锁状态仍然为一个int类型,在java中,int类型4个字节32位。我们利用高16位代表读锁获取次数,低16位代表写锁获取次数。

当前同步状态表示一个线程已经获取了写锁,且重进入了两次,同时也连续获取了两次读锁。读写锁是如何迅速确定读和写各自的状态呢?答案是通过位运算。假设当前同步状态值为S,写状态等于S&0x0000FFFF(将高16位全部抹去),读状态等于S>>>16(无符号补0右移

16位)。当写状态增加1时,等于S+1,当读状态增加1时,等于S+(1<>>16)大于0,即读锁已被获取。

static final intSHARED_SHIFT

= 16;

static final intSHARED_UNIT

= (1 <

static final intMAX_COUNT

= (1 <

static final intEXCLUSIVE_MASK = (1 <

具体实现参加Sync类,这里的Sync类中包含了许多线程读写锁计数属性,这里按下不表,关键是实现了tryAcquire、tryAcquireShared、tryRelease、tryReleaseShared等独占锁和共享锁方法。

2.

读锁的获取和释放

1)

获取

读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问(或者写状态为0)时,读锁总会被成功地获取,而所做的也只是(线程安全的)增加读状态。如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态。获取读锁的实现从Java 5到Java 6变得复杂许多,主要原因是新增了一些功能,例如getReadHoldCount()方法,作用是返回当前线程获取读锁的次数。读状态是所有线程获取读锁次数的总和,而每个线程各自获取读锁的次数只能选择保存在ThreadLocal中,由线程自身维护,这使获取读锁的实现变得复杂。

如果发现无法获取读锁tryAcquireShared(CAS原因或其他线程已获取写锁),当前线程进入AQS的doAcquireShared方法自旋,循环第二次仍然无法获取读锁就将前序节点的waitStatus置为SIGNAL,当前线程挂起。在doAcquireShared方法中,如果成功获取读锁,那么会调用setHeadAndPropagate方法,将当前线程节点设置为同步队列head,并对节点特性为共享shared的后继节点调用doReleaseShared方法,如果当前节点waitStatus处于等待状态(SIGNAL),那么试图唤醒该线程;如果waitStatus为0,那么将其改为传播状态(PROPAGATE)。

一旦shared节点被唤醒,就会继续执行doAcquireShared方法中的自旋,对于紧接head的节点,会继续执行setHeadAndPropagate方法,从而唤醒它的下一个后继节点。这样,就实现了整个同步队列中处于等待状态的共享节点链式反应(唤醒),直到唤醒独占节点,然后开始进入读锁CAS获取过程。

以下为ReentryReadWriteLock的Sync类中实现的独占锁获取方法tryAcquireShared:

protected final inttryAcquireShared(intunused) {Thread current = Thread.currentThread();

intc = getState();

if(exclusiveCount(c) !=0&&

getExclusiveOwnerThread() != current)return-1;

intr = 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{

HoldCounterrh =cachedHoldCounter;

if(rh ==null|| rh.tid!= getThreadId(current))cachedHoldCounter= rh =readHolds.get();

else if(rh.count==0)readHolds.set(rh);rh.count++;}return1;}returnfullTryAcquireShared(current);}final intfullTryAcquireShared(Thread current) {HoldCounterrh =null;

for(;;) {intc = getState();

if(exclusiveCount(c) !=0) {if(getExclusiveOwnerThread() != current)return-1;// else we hold the exclusive lock; blocking here

// would cause deadlock.}else if(readerShouldBlock()) {// Make sure we're not acquiring read lock reentrantlyif(firstReader== current) {// assert firstReaderHoldCount> 0;}else{if(rh ==null) {

rh =cachedHoldCounter;

if(rh ==null|| rh.tid!= getThreadId(current)) {

rh =readHolds.get();

if(rh.count==0)readHolds.remove();}

}if(rh.count==0)return-1;}

}if(sharedCount(c) ==MAX_COUNT)throw newError("Maximum lock count exceeded");

if(compareAndSetState(c,c +SHARED_UNIT)) {if(sharedCount(c) ==0) {firstReader= current;firstReaderHoldCount=1;}else if(firstReader== current) {firstReaderHoldCount++;}else{if(rh ==null)

rh =cachedHoldCounter;

if(rh ==null|| rh.tid!= getThreadId(current))

rh =readHolds.get();

else if(rh.count==0)readHolds.set(rh);rh.count++;cachedHoldCounter= rh;// cache for release}return1;}

}

}

2)

释放

读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的值是(1<<16)。读锁释放时,会唤醒所有等待状态为SIGNAL的后继节点(线程处于waiting状态),包括写锁线程。

3.

写锁的获取和释放

1)

获取

写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态。

该方法除了重入条件(当前线程为获取了写锁的线程)之外,增加了一个读锁是否存在的判断。如果存在读锁,则写锁不能被获取,原因在于:读写锁要确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。因此,只有等待其他读线程都释放了读锁,写锁才能被当前线程获取,而写锁一旦被获取,则其他读写线程的后续访问均被阻塞。

以下为ReentryReadWriteLock的Sync类中实现的独占锁获取方法tryAcquire:

protected final booleantryAcquire(intacquires) {Thread current = Thread.currentThread();

intc = getState();

intw = exclusiveCount(c);

if(c !=0) {// (Note: if c != 0 and w == 0 then shared count != 0)if(w ==0|| current != getExclusiveOwnerThread())return false;

if(w + exclusiveCount(acquires) >MAX_COUNT)throw newError("Maximum lock count exceeded");// Reentrant acquiresetState(c + acquires);

return true;}if(writerShouldBlock() ||

!compareAndSetState(c,c + acquires))return false;setExclusiveOwnerThread(current);

return true;}

2)

释放

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

以下为ReentryReadWriteLock的Sync类中实现的独占锁获取方法tryRelease:

protected final booleantryRelease(intreleases) {if(!isHeldExclusively())throw newIllegalMonitorStateException();

intnextc = getState() - releases;

booleanfree = exclusiveCount(nextc) ==0;

if(free)

setExclusiveOwnerThread(null);setState(nextc);

returnfree;}

4.

锁降级

锁降级指的是写锁降级成为读锁。如果当前线程拥有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。

锁降级是指把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。

接下来看一个锁降级的示例。因为数据不常变化,所以多个线程可以并发地进行数据处理,当数据变更后,如果当前线程感知到数据变化,则进行数据的准备工作,同时其他处理线程被阻塞,直到当前线程完成数据的准备工作。

如果没有红框内的读锁,一旦释放了写锁,其他并发线程同步修改了数据,下面的try(使用数据的流程)中的数据就不准确了,所以必须在写锁释放前加上读锁,以保证在使用数据时其他线程不能修改数据update。

三、

使用

读写锁的使用非常简单,只需要取出读锁和写锁,分别在读和写的时候加锁即可。

@Slf4jpublic classMyReentryReadWriteLockTestextendsAlgorithmApplicationTests{private staticSyncObjectsyncObject=newSyncObject();@Testpublic voidtest()throwsInterruptedException {for(inti =0;i<20;i++) {

Thread readThread =newThread(newMyReadLockThread());Thread writeThread =newThread(newMyWriteLockThread());readThread.start();writeThread.start();}

Thread.sleep(2000);}private static classMyReadLockThreadimplementsRunnable{@Overridepublic voidrun() {log.info("read-count-{}",syncObject.get());}

}private static classMyWriteLockThreadimplementsRunnable{@Overridepublic voidrun() {log.info("write-count-{}",syncObject.increment());}

}private static classSyncObject {private inti=0;ReentrantReadWriteLockreadWriteLock=newReentrantReadWriteLock();ReentrantReadWriteLock.ReadLockreadLock=readWriteLock.readLock();ReentrantReadWriteLock.WriteLockwriteLock=readWriteLock.writeLock();

private staticThreadLocalcountLocal =newThreadLocal<>();

public intincrement() {writeLock.lock();

try{

countLocal.set(countLocal.get() ==null?0: countLocal.get() +1);log.info("write-before-local-{}",countLocal.get());Thread.sleep(50);i++;

returni;}catch(Exception e) {

e.printStackTrace();i++;

returni;}finally{writeLock.unlock();}

}public intget() {readLock.lock();

try{

Thread.sleep(50);

returni;}catch(Exception e) {

e.printStackTrace();

returni;}finally{readLock.unlock();}

}

}

}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值