共享锁和排它锁(ReentrantReadWriteLock)

1、什么是共享锁和排它锁

共享锁就是允许多个线程同时获取一个锁,一个锁可以同时被多个线程拥有。

排它锁,也称作独占锁,一个锁在某一时刻只能被一个线程占有,其它线程必须等待锁被释放之后才可能获取到锁。

2、排它锁和共享锁实例

ReentrantLock就是一种排它锁。CountDownLatch是一种共享锁。这两类都是单纯的一类,即,要么是排它锁,要么是共享锁。

ReentrantReadWriteLock是同时包含排它锁和共享锁特性的一种锁,这里主要以ReentrantReadWriteLock为例来进行分析学习。我们使用ReentrantReadWriteLock的写锁时,使用的便是排它锁的特性;使用ReentrantReadWriteLock的读锁时,使用的便是共享锁的特性。

3、锁的等待队列组成

ReentrantReadWriteLock有一个读锁(ReadLock)和一个写锁(WriteLock)属性,分别代表可重入读写锁的读锁和写锁。有一个Sync属性来表示这个锁上的等待队列。ReadLock和WriteLock各自也分别有一个Sync属性表示在这个锁上的队列

通过构造函数来看,

    public ReentrantReadWriteLock(boolean fair)
{

sync = (fair)? new FairSync()
new NonfairSync();

readerLock = new ReadLock(this);

writerLock = new WriteLock(this);

}

在创建读锁和写锁对象的时候,会把这个可重入的读写锁上的Sync属性传递过去。

protected ReadLock(ReentrantReadWriteLock
lock) {

sync = lock.sync;

}

protected WriteLock(ReentrantReadWriteLock
lock) {

sync = lock.sync;

}

所以,最终的效果是读锁和写锁使用的是同一个线程等待队列。这个队列就是通过我们在前面介绍过的AbstractQueuedSynchronizer实现的。

4、锁的状态

既然读锁和写锁使用的是同一个等待队列,那么这里要如何区分一个锁的读状态(有多少个线程正在读这个锁)和写状态(是否被加了写锁,哪个线程正在写这个锁)。

首先每个锁都有一个exclusiveOwnerThread属性,这是继承自AbstractQueuedSynchronizer,来表示当前拥有这个锁的线程。那么,剩下的主要问题就是确定,有多少个线程正在读这个锁,以及是否加了写锁。

这里可以通过线程获取锁时执行的逻辑来看,下面是线程获取读锁时会执行的一部分代码。

final boolean tryReadLock()
{

Thread current = Thread.currentThread();

for (;;)
{

int c
= getState();

if (exclusiveCount(c)
!= 0 &&

getExclusiveOwnerThread() != current)

return false ;

if (sharedCount(c)
== MAX_COUNT)

throw new Error("Maximum
lock count exceeded" );

if (compareAndSetState(c,
c + SHARED_UNIT)) {

HoldCounter rh = cachedHoldCounter;

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

cachedHoldCounter = rh = readHolds.get();

rh.count++;

return true ;

}

}

}

注意这个函数的调用exclusiveCount(c) ,用来计算这个锁当前的写加锁次数(同一个进程多次进入会累加)。代码如下

/** Returns the number of shared holds represented in count  */

static int sharedCount( int c)   
return c >>> SHARED_SHIFT; }

/** Returns the number of exclusive holds
represented in count  */

static int exclusiveCount (int c)
return c & EXCLUSIVE_MASK; }

相关常量的定义如下

static final int SHARED_SHIFT  
= 16;

static final int EXCLUSIVE_MASK =
(1 << SHARED_SHIFT) - 1;

如果从二进制来看EXCLUSIVE_MASK的表示,这个值的低16位全是1,而高16位则全是0,所以exclusiveCount是把state的低16位取出来,表示当前这个锁的写锁加锁次数。

再来看sharedCount,取出了state的高16位,用来表示这个锁的读锁加锁次数。所以,这里是用state的高16位和低16位来分别表示这个锁上的读锁和写锁的加锁次数。

现在再回头来看tryReadLock实现,首先检查这个锁上是否被加了写锁,同时检查加写锁的是不是当前线程。如果不是被当前线程加了写锁,那么试图加读锁就失败了。如果没有被加写锁,或者是被当前线程加了写锁,那么就把读锁加锁次数加1,通过compareAndSetState(c,
c + SHARED_UNIT)来实现

SHARED_UNIT的定义如下,刚好实现了高16位的加1操作。

static final int SHARED_UNIT   
= (1 << SHARED_SHIFT);

5、线程阻塞和唤醒的时机

线程的阻塞和访问其他锁的时机相似,在线程视图获取锁,但这个锁又被其它线程占领无法获取成功时,线程就会进入这个锁对象的等待队列中,并且线程被阻塞,等待前面线程释放锁时被唤醒。

但因为加读锁和加写锁进入等待队列时存在一定的区别,加读锁时,final Node
node = addWaiter(Node.SHARED);节点的nextWaiter指向一个共享节点,表明当前这个线程是处于共享状态进入等待队列。

加写锁时如下,

public final void acquire (int arg)
{

if (!tryAcquire(arg)
&&

acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

selfInterrupt();

}

线程是处于排它状态进入等待队列的。

在线程的阻塞上,读锁和写锁的时机相似,但在线程的唤醒上,读锁和写锁则存在较大的差别。

读锁通过AbstractQueuedSynchronizer的doAcquireShared来完成获取锁的动作。

private void doAcquireShared( int arg)
{

final Node
node = addWaiter(Node.SHARED);

try {

boolean interrupted
false;

for (;;)
{

final Node
p = node.predecessor();

if (p
== head) {

int r
= tryAcquireShared(arg);

if (r
>= 0) {

setHeadAndPropagate(node, r);

p.next = null ; //
help GC

if (interrupted)

selfInterrupt();

return ;

}

}

if (shouldParkAfterFailedAcquire(p,
node) &&

parkAndCheckInterrupt())

interrupted = true ;

}

catch (RuntimeException
ex) {

cancelAcquire(node);

throw ex;

}

}

在tryAcquireShared获取读锁成功后(返回正数表示获取成功),有一个setHeadAndPropagate的函数调用。

写锁通过AbstractQueuedSynchronizer的acquire来实现锁的获取动作。

public final void acquire( int arg)
{

if (!tryAcquire(arg)
&&

acquireQueued(addWaiter(Node.EXCLUSIVE),
arg))

selfInterrupt();

}

如果tryAcquire获取成功则直接返回,否则把线程加入到锁的等待队列中。和一般意义上的ReentrantLock的原理一样。

所以在加锁上,主要的差别在于这个setHeadAndPropagate方法,其代码如下

private void setHeadAndPropagate (Node
node, int propagate) {

Node h = head; // Record old head for check
below

setHead(node);

/*

* Try to signal next queued node if:

* Propagation was indicated by caller,

* or was recorded (as h.waitStatus) by a previous operation

* (note: this uses sign-check of waitStatus because

* PROPAGATE status may transition to SIGNAL.)

* and

* The next node is waiting in shared mode,

* or we don‘t know, because it appears null

*

* The conservatism in both of these checks may cause

* unnecessary wake-ups, but only when there are multiple

* racing acquires/releases, so most need signals now or soon

* anyway.

*/

if (propagate
> 0 || h == null || h.waitStatus < 0)
{

Node s = node.next;

if (s
== null || s.isShared())

doReleaseShared();

}

}

主要操作是把这个节点设为头节点(成为头节点,则表示不在等待队列中,因为获取锁成功了),同时释放锁(doReleaseShared)。

下面来看doReleaseShared的实现

private void doReleaseShared()
{

/*

* Ensure that a release propagates, even if there are other

* in-progress acquires/releases. This proceeds in the usual

* way of trying to unparkSuccessor of head if it needs

* signal. But if it does not, status is set to PROPAGATE to

* ensure that upon release, propagation continues.

* Additionally, we must loop in case a new node is added

* while we are doing this. Also, unlike other uses of

* unparkSuccessor, we need to know if CAS to reset status

* fails, if so rechecking.

*/

for (;;)
{

Node h = head;

if (h
!= null && h != tail) {

int ws
= h.waitStatus;

if (ws
== Node.SIGNAL) {

if (!compareAndSetWaitStatus(h,
Node.SIGNAL, 0))

continue ; //
loop to recheck cases

unparkSuccessor(h);

}

else if (ws
== 0 &&

!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))

continue ; //
loop on failed CAS

}

if (h
== head) // loop if head changed

break ;

}

}

把头节点的waitStatus这只为0或者Node.PROPAGATE,并且唤醒下一个线程,然后就结束了。

总结一下,就是一个线程在获取读锁后,会唤醒锁的等待队列中的第一个线程。如果这个被唤醒的线程是在获取读锁时被阻塞的,那么被唤醒后,就会在for循环中,又执行到setHeadAndPropagate,这样就实现了读锁获取时的传递唤醒。这种传递在遇到一个因为获取写锁被阻塞的线程节点时被终止。

6、读线程之间的唤醒

如果一个线程在共享模式下获取了锁状态,这个时候,它是否要唤醒其它在共享模式下等待在该锁上的线程?

由于多个线程可以同时获取共享锁而不相互影响,所以,当一个线程在共享状态下获取了锁之后,理论上是可以唤醒其它在共享状态下等待该锁的线程。但如果这个时候,在这个等待队列中,既有共享状态的线程,同时又有排它状态的线程,这个时候又该如何唤醒?

实际上对于锁来说,在共享状态下,一个线程无论是获取还是释放锁的时候,都会试着去唤醒下一个等待在这个锁上的节点(通过上面的doAcquireShared代码能看出)。如果下一个线程也是处于共享状态等待在锁上,那么这个线程就会被唤醒,然后接着试着去唤醒下一个等待在这个锁上的线程,这种唤醒动作会一直持续下去,直到遇到一个在排它状态下阻塞在这个锁上的线程,或者等待队列全部被释放为止。

因为线程是在一个FIFO的等待队列中,所以,这这样一个一个往后传递,就能保证唤醒被传递下去。

参考资料   http://www.liuinsect.com/2014/09/04/jdk1-8-abstractqueuedsynchronizer-part2/

时间: 2024-09-28 20:27:34

共享锁和排它锁(ReentrantReadWriteLock)的相关文章

java 实现共享锁和排它锁

一直对多线程有恐惧,在实现共享锁和排它锁之后,感觉好了很多. 共享锁    就是查询的时候,如果没有修改,可以支持多线程查询: 排它锁    就是修改的时候,锁定共享锁,停止查询,同时,锁定排它锁,只允许一个线程进行修改,修改完成后,再解开共享锁: 心路历程: 一开始的想法是,绝对不调用 sleep ,要用到sleep 的地方我觉得都可以用  notify() 和 wait() 替换,sleep(T) 我觉得T  的大小压根是无法算出来的,如果非要用sleep 我觉得T 的时间应该是一个动态的值

共享锁与排它锁区别(转)

共享锁[S锁]又称读锁,若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁.这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改. 排他锁[X锁]又称写锁.若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁.这保证了其他事务在T释放A上的锁之前不能再读取和修改A. 共享锁与排它锁区别 1.共享锁只用于表级,排他锁用于行级. 2.加了共享锁的对象,可以继续加共

对mysql乐观锁、悲观锁、共享锁、排它锁、行锁、表锁概念的理解

操作数据库的时候,可能会由于并发问题而引起的数据的不一致性(数据冲突) ----乐观锁 乐观锁不是数据库自带的,需要我们自己去实现.乐观锁是指操作数据库时(更新操作),想法很乐观,认为这次的操作不会导致冲突,在操作数据时,并不进行任何其他的特殊处理(也就是不加锁),而在进行更新后,再去判断是否有冲突了. 通常实现是这样的:在表中的数据进行操作时(更新),先给数据表加一个版本(version)字段,每操作一次,将那条记录的版本号加1.也就是先查询出那条记录,获取出version字段,如果要对那条记

MySql共享锁和排它锁

共享锁和排他锁 1.共享锁: 读锁.X锁,在查询时生效,多个事务在查询同一个数据时共享一把锁,但是不能作用于修改数据,在select语句后添加 lock in share mode : 2.排他锁:在insert.update和delete时生效,获取排他锁的事务在修改一条数据时,其他锁不能存在于这条数据上(如其它排它锁和共享锁),也就是说只有获取了排他锁的这个事务可以操作修改数据,IMySQL InnoDB引擎默认添加排他锁. 场景 参考:https://www.cnblogs.com/bob

mysql共享锁与排它锁

共享锁shared lock(也叫读锁read lock)又称读锁,若事务T对数据对象A加上S锁,则事务T可以读A但不能修改A,其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁.这保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改.共享锁不阻塞共享锁,阻塞排它锁 排他锁exclusive lock(也叫writer lock)又称写锁.若事务T对数据对象A加上X锁,事务T可以读A也可以修改A,其他事务不能再对A加任何锁,直到T释放A上的锁.这保证了其他事务在T释放A上

MySQL中的锁(表锁、行锁,共享锁,排它锁,间隙锁)

本文参考: http://mysqlpub.com/thread-5383-1-1.html http://blog.csdn.net/c466254931/article/details/53463596 有很多是转载合并过来. 锁是计算机协调多个进程或线程并发访问某一资源的机制.在数据库中,除传统的 计算资源(如CPU.RAM.I/O等)的争用以外,数据也是一种供许多用户共享的资源.如何保证数据并发访问的一致性.有效性是所有数据库必须解决的一 个问题,锁冲突也是影响数据库并发访问性能的一个重

共享锁和排它锁

共享锁(S锁):如果事务T对数据A加上共享锁后,则其他事务只能对A再加共享锁,不能加排他锁. 获准共享锁的事务职能读取数据,不能修改数据. 排他锁(X锁):如果事务T对数据A加上排他锁后,则其他事务不能在对A加任何类型的封锁. 获准排他锁的事务既能读取数据,也能修改数据. 数据库死锁的原因:若干事务相互等待对方释放封锁,就陷入了无限期等待状态,系统进入死锁. 预防数据库死锁的方法:1.要求一个事务必须一次性封锁所需要的所有数据(要么全成功,要么全部成功) 2.规定封锁数据的顺序,所有事务必须按照

MySQL InnoDB存储引擎排它锁和共享锁的研究

1,共享锁实验 session1 在session1建表lisa并插入数据 mysql> create table lisa(name char(10),age int(5)); mysql> insert into lisa values('lisa','26'); 加给age=26这一行加共享锁 mysql> set autocommit=0; mysql> select * from lisa where age=26 lock in share mode; mysql>

共享锁(S锁)和排它锁(X锁)

释义 共享锁:(读取)操作创建的锁.其他用户可以并发读取数据,但任何事物都不能获取数据上的排它锁,直到已释放所有共享锁. 共享锁(S锁)又称为读锁,若事务T对数据对象A加上S锁,则事务T只能读A:其他事务只能再对A加S锁,而不能加X锁,直到T释放A上的S锁.这就保证了其他事务可以读A,但在T释放A上的S锁之前不能对A做任何修改. 排它锁 排它锁:排它锁又称为写锁((eXclusive lock,简记为X锁)),若事物T对数据对象A加上X锁,则只允许T读取和修改A,其它任何事务都不能再对A加任何类