1. 概念

共享锁【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。

对比

  • 共享锁就是允许多个线程同时获取一个锁,一个锁可以同时被多个线程拥有。
  • 排它锁,也称作独占锁,一个锁在某一时刻只能被一个线程占有,其它线程必须等待锁被释放之后才可能获取到锁。

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© ,用来计算这个锁当前的写加锁次数(同一个进程多次进入会累加)。代码如下

/** 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 GCif (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 belowsetHead(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 casesunparkSuccessor(h);}else if (ws == 0 &&!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))continue ; // loop on failed CAS}if (h == head) // loop if head changedbreak ;}}

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

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

下面通过代码来理解这种等待和线程唤醒顺序。

package lynn.lock;import java.util.concurrent.locks.ReentrantReadWriteLock;public class TestThread extends Thread {private ReentrantReadWriteLock lock;private String threadName;private boolean isWriter ;public TestThread(ReentrantReadWriteLock lock, String name, boolean isWriter) {this.lock = lock;this.threadName = name;this.isWriter = isWriter;}@Overridepublic void run() {while (true ) {try {if (isWriter ) {lock.writeLock().lock();} else {lock.readLock().lock();}if (isWriter ) {Thread. sleep(3000);System. out.println("----------------------------" );}System. out.println(System.currentTimeMillis() + ":" + threadName );if (isWriter ) {Thread. sleep(3000);System. out.println("-----------------------------" );}} catch (Exception e) {e.printStackTrace();} finally {if (isWriter ) {lock.writeLock().unlock();} else {lock.readLock().unlock();}}break;}}}

TestThread是一个自定义的线程类,在生成线程的时候,需要传递一个可重入的读写锁对象进去,线程在执行时会先加锁,然后进行内容输出,然后释放锁。如果传递的是写锁,那线程在输出结果前后会先沉睡3秒,便于区分输出的结果时间。

package lynn.lock;import java.util.concurrent.locks.ReentrantReadWriteLock;public class Main {public static void blockByWriteLock() {ReentrantReadWriteLock lock = new ReentrantReadWriteLock();lock.writeLock().lock();TestThread[] threads = new TestThread[10];for (int i = 0; i < 10; i++) {boolean isWriter = (i + 1) % 4 == 0 ? true : false;TestThread thread = new TestThread(lock, "thread-" + (i + 1), isWriter);threads[i] = thread;}for (int i = 0; i < threads.length; i++) {threads[i].start();}System. out.println(System.currentTimeMillis() + ": block by write lock");try {Thread. sleep(3000);} catch (Exception e) {e.printStackTrace();}lock.writeLock().unlock();}public static void main(String[] args) {blockByWriteLock();}
}

在Main中构造了10个线程,由于这个锁一开始是被主线程拥有,并且是在排它状态下加锁的,所以我们构造的10个线程,在一开始执行便是按照其编号从小到大在等待队列中(1到10)。然后主线程打印结果,等待3秒后释放锁。由于前3个线程,编号1到3是处于共享状态阻塞的,而第4个线程是处于排它状态阻塞,所以,按照上面的唤醒顺序,唤醒传递到第4个线程时就结束。

依次类推,理论上的打印顺序是 :主线程 [1,2,3] 4 [5,6,7] 8 [9,10]

从下面的执行结果来看,也是符合我们的预期的。

6、读线程之间的唤醒

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

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

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

详解共享锁(S锁)和排它锁(X锁)相关推荐

  1. python gil 解除_详解Python中的GIL(全局解释器锁)详解及解决GIL的几种方案

    先看一道GIL面试题: 描述Python GIL的概念, 以及它对python多线程的影响?编写一个多线程抓取网页的程序,并阐明多线程抓取程序是否可比单线程性能有提升,并解释原因. GIL:又叫全局解 ...

  2. 数据库锁的详解, 共享锁, 更新锁, 排它锁, 意向锁, 加锁原理

    一,锁的种类 1.共享锁--Shared lock 又称读锁(S锁),共享锁不阻塞其他事务的读操作,但阻塞写操作,同一数据对象A可以共存多个共享锁,这被称为共享锁兼容. 当T1为数据对象A加上共享锁后 ...

  3. mysql 事务 注意 优化_MySQL入门详解(二)---mysql事务、锁、以及优化

    MySQL 事务主要用于处理操作量大,复杂度高的数据.比如说,在一个商城系统中,用户执行购买操作,那么用户订单中应该加一条,库存要减一条,如果这两步由于意外只进行了其中一步那么就会发生很大的问题.而事 ...

  4. 详解ReentrantLock为什么是可重入锁

    1 缘起 有一次,公司有人在面试,路过时,听到面试官问到了锁, 让面试者聊一聊用到的锁, 我此时,也是心里一震, 我用过哪些锁?为什么使用? 搜索了好一会儿,哈哈哈,我就是这么菜. 只学习过synch ...

  5. Mysql 死锁过程及案例详解之插入意向锁与自增锁备份锁日志锁Insert Intention Lock Auto-increment Lock Backup Lock Log Lock

    Mysql 插入意向锁与自增锁备份锁日志锁 插入意向锁Insert Intention Lock 插入意向锁Insert intention locks是记录级别的,它通过"INSERT&q ...

  6. Mysql 死锁过程及案例详解之显式与隐式锁Explicit Table Lock Implicit Table Lock

    显式锁Explicit Table Lock与隐式锁Explicit Table Lock 显式锁Explicit Table Lock 显式表锁(Explicit Table Locks)即通过命令 ...

  7. 一文详解 Java 的几把 JVM 级锁

    作者 | 楚昭 来源 | 阿里巴巴中间件(ID:Aliware_2018) 在计算机行业有一个定律叫"摩尔定律",在此定律下,计算机的性能突飞猛进,而且价格也随之越来越便宜, CP ...

  8. java中间件登陆超时_一文详解 Java 的几把 JVM 级锁

    作者 | 楚昭 来源 | 阿里巴巴中间件(ID:Aliware_2018) 在计算机行业有一个定律叫"摩尔定律",在此定律下,计算机的性能突飞猛进,而且价格也随之越来越便宜, CP ...

  9. 详解PostgreSQL数据库中的两阶段锁

    点击上方"蓝字" 关注我们,享更多干货! 数据库中的对象是共享的,假如不同的用户同时修改某个对象,就会出现数据错乱,从而破坏数据库的数据一致性,违反事务的隔离性原则. 为了满足隔离 ...

最新文章

  1. python-深浅copy-18
  2. Mysql数据库设计及常见问题
  3. 改变Linux工作环境中的提示信息
  4. 判断端口是否能用_【图文】 Windows自带入侵检测工具—Netstat命令查询 是否中木马...
  5. python和php合成,Python照片合成的方法详解
  6. 中国工商银行贵金属递延如何销户
  7. mysql创建的是拉丁_mysql 拉丁1 转换成 utf8
  8. 2016-2017 ACM-ICPC Southwestern European Regional Programming Contest (SWERC 2016)
  9. mysql和mysqli的区别
  10. python二叉树的非递归遍历
  11. matlab函数coth,matlab函数
  12. iOS开发之音视频边下边播缓存方案
  13. 手机邮箱怎么发送电子邮件?163邮箱登陆界面好看么?
  14. BurpSuite使用详解(三)Spider功能
  15. 诺奖背后的一位女性:伯莎·冯·苏特娜
  16. 罗克韦尔AB PLC RSLogix5000中的位指令使用方法介绍
  17. 【C语言趣味编程100题】
  18. OpenCV图像处理形态学操作腐蚀Erode与膨胀Dilate
  19. 拼多多客户差评回复话术
  20. Python学习 | pymysql操作数据库?真原生...

热门文章

  1. 【图片编辑小软件, 在线文件转换器】FastStone Photo Resizer支持批量转换和批量重命名;免费快速在线转换器, 将pdf, 图像, 视频, 文档, 音频, 电子书及压缩等格式相互转换
  2. 抛物型偏微分方程的Crank-Nicolson 方法; Richardson 外推法;紧差分法
  3. fastposter 1.6.0 发布 电商级海报生成器
  4. python画八卦_python编程也能八卦?
  5. python系列11:python的游戏引擎
  6. c语言消消乐字母游戏代码,基于pygame的小游戏———数字消消乐
  7. 图像处理之Mean Shift滤波(边缘保留的低通滤波)
  8. 11 月中国手游海外收入排行:米哈游《原神》第一,《使命召唤手游》第二
  9. Robcup2D足球学习记录【2020.01.14】
  10. STM32Cube程序使用 DFU 烧写后Leave DFUMode无法运行程序