读写锁定义:读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读
线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。

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

1. 读写锁接口

ReadWriteLock仅定义了获取读锁和写锁的两个方法,即readLock()方法和writeLock()方法

2.读写锁的实现机制原理分析

2.1 读写状态设计

同步状态表示锁被一个线程重复获取的次数,而读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态,使得该状态的设计成为读写锁实现的关键。如果在一个整型变量上维护多种状态,就一定需要“按位切割使用”这个变量,读写锁将变量切分成了两个部分,高16位表示读,低16位表示写

读写锁是如何迅速确定读和写各自的状态呢?

答案是通过位运算。假设当前同步状态值为S,写状态等于S&0x0000FFFF(将高16位全部抹去),读状态等于S>>>16(无符号补0右移16位)。当写状态增加1时,等于S+1,当读状态增加1时,等于S+(1<<16),也就是
S+0x00010000。

2.2 写锁的获取与释放

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

2.2.1 获取写锁的过程

下面我们来看读写锁中写锁获取同步状态过程:

ReentrantReadWriteLock
public void lock() {sync.acquire(1);}AbstractQueuedSynchronizer
public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}ReentrantReadWriteLock
protected final boolean tryAcquire(int acquires) {/** Walkthrough:* 1. If read count nonzero or write count nonzero*    and owner is a different thread, fail.* 2. If count would saturate, fail. (This can only*    happen if count is already nonzero.)* 3. Otherwise, this thread is eligible for lock if*    it is either a reentrant acquire or*    queue policy allows it. If so, update state*    and set owner.*/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())return false;if (w + exclusiveCount(acquires) > MAX_COUNT)throw new Error("Maximum lock count exceeded");// Reentrant acquiresetState(c + acquires);return true;}if (writerShouldBlock() ||!compareAndSetState(c, c + acquires))return false;setExclusiveOwnerThread(current);return true;
}

int w = exclusiveCount©;方法获取独占的状态,也就是写锁的状态。if (c != 0) 和(w == 0) 意思就是说状态不为0,并且写锁的状态为0,那么就是读锁的状态不为0.说明当前线程存在读锁。

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

2.2.2 获取读锁的过程

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

2.3 读锁的获取与释放

2.3.1 获取读锁的过程

读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问
(或者写状态为0)时,读锁总会被成功地获取,而所做的也只是(线程安全的)增加读状态。如果当前线程已经获取了读锁,则增加读状态。

下面我们来看一下源码中读锁的获取过程:

ReentrantReadWriteLockpublic void lock() {sync.acquireShared(1);}AbstractQueuedSynchronizer
public final void acquireShared(int arg) {if (tryAcquireShared(arg) < 0)doAcquireShared(arg);}ReentrantReadWriteLock
protected final int tryAcquireShared(int unused) {/** Walkthrough:* 1. If write lock held by another thread, fail.* 2. Otherwise, this thread is eligible for*    lock wrt state, so ask if it should block*    because of queue policy. If not, try*    to grant by CASing state and updating count.*    Note that step does not check for reentrant*    acquires, which is postponed to full version*    to avoid having to check hold count in*    the more typical non-reentrant case.* 3. If step 2 fails either because thread*    apparently not eligible or CAS fails or count*    saturated, chain to version with full retry loop.*/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);
}

在tryAcquireShared(int unused)方法中,如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠CAS保证)增加读状态,成功获取读锁。

读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的
值是(1<<16)。

2.4 锁降级

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

接下来看一个锁降级的示例:

public void processData() {readLock.lock();if (!update) {// 必须先释放读锁readLock.unlock();// 锁降级从写锁获取到开始writeLock.lock();try {if (!update) {// 准备数据的流程(略)update = true;}readLock.lock();} finally {writeLock.unlock();}// 锁降级完成,写锁降级为读锁}try {// 使用数据的流程(略)} finally {readLock.unlock();}}

锁降级中读锁的获取是否必要呢?

答案是必要的。主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记作线程T)获取了写锁并修改了数据,那么当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。

3. LockSupport工具

LockSupport定义了一组的公共静态方法,这些方法提供了最基本的线程阻塞和唤醒功
能,而LockSupport也成为构建同步组件的基础工具。LockSupport定义了一组以park开头的方法用来阻塞当前线程,以及unpark(Thread thread)方法来唤醒一个被阻塞的线程。

下面我们来看一下LockSupport提供的阻塞和唤醒方法

4. Condition接口

前面我们了解到Object类中含有wait()、wait(long timeout)、notify()、notifyAll()这些方法与synchronized同步关键字配合,可以实现等待/通知模式。Condition接口同样和Lock锁结合也可以是实现等待/通知模式。这是一种Java API层的等待/通知模型。

4.1 Condition接口

Condition定义了等待/通知两种类型的方法

获取一个Condition必须通过Lock的newCondition()方法。

4.2 Condition实现原理

ConditionObject是同步器AbstractQueuedSynchronizer的内部类,因为Condition的操作需要获取相关联的锁,所以作为同步器的内部类也较为合理。每个Condition对象都包含着一个队列(以下称为等待队列),该队列是Condition对象实现等待/通知功能的关键。

下面将分析Condition的实现,主要包括:等待队列、等待和通知

4.2.1 等待队列

等待队列是一个FIFO的队列,在队列中的每个节点都包含了一个线程引用,该线程就是
在Condition对象上等待的线程,如果一个线程调用了Condition.await()方法,那么该线程将会释放锁、构造成节点加入等待队列并进入等待状态。事实上,节点的定义复用了同步器中节点的定义,也就是说,同步队列和等待队列中节点类型都是同步器的静态内部类
AbstractQueuedSynchronizer.Node。

一个Condition包含一个等待队列,Condition拥有首节点(firstWaiter)和尾节点
(lastWaiter)。当前线程调用Condition.await()方法,将会以当前线程构造节点,并将节点从尾部加入等待队列,等待队列的基本结构如图:

如图所示,Condition拥有首尾节点的引用,而新增节点只需要将原有的尾节点nextWaiter
指向它,并且更新尾节点即可。上述节点引用更新的过程并没有使用CAS保证,原因在于调用
await()方法的线程必定是获取了锁的线程,也就是说该过程是由锁来保证线程安全的.

在Object的监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包中的Lock(更确切地说是同步器)拥有一个同步队列和多个等待队列。其对应关系如下图所示:

4.2.2 等待

调用Condition的await()方法(或者以await开头的方法),会使当前线程进入等待队列并释放锁,同时线程状态变为等待状态。当从await()方法返回时,当前线程一定获取了Condition相关联的锁。

如果从队列(同步队列和等待队列)的角度看await()方法,当调用await()方法时,相当于同步队列的首节点(获取了锁的节点)移动到Condition的等待队列中。

AbstractQueuedSynchronizer.ConditionObject
public final void await() throws InterruptedException {if (Thread.interrupted())throw new InterruptedException();Node node = addConditionWaiter();int savedState = fullyRelease(node);int interruptMode = 0;while (!isOnSyncQueue(node)) {LockSupport.park(this);if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)break;}if (acquireQueued(node, savedState) && interruptMode != THROW_IE)interruptMode = REINTERRUPT;if (node.nextWaiter != null) // clean up if cancelledunlinkCancelledWaiters();if (interruptMode != 0)reportInterruptAfterWait(interruptMode);}

调用该方法的线程成功获取了锁的线程,也就是同步队列中的首节点,该方法会将当前
线程构造成节点并加入等待队列中,然后释放同步状态,唤醒同步队列中的后继节点,然后当前线程会进入等待状态。

当等待队列中的节点被唤醒,则唤醒节点的线程开始尝试获取同步状态

4.2.3 通知

调用Condition的signal()方法,将会唤醒在等待队列中等待时间最长的节点(首节点),在唤醒节点之前,会将节点移到同步队列中。

public final void signal() {if (!isHeldExclusively())throw new IllegalMonitorStateException();Node first = firstWaiter;if (first != null)doSignal(first);}

调用该方法的前置条件是当前线程必须获取了锁,可以看到signal()方法进行了isHeldExclusively()检查,也就是当前线程必须是获取了锁的线程。接着获取等待队列的首节点,将其移动到同步队列并使用LockSupport唤醒节点中的线程。

节点从等待队列中移动到同步队列的过程:

通过调用同步器的enq(Node node)方法,等待队列中的头节点线程安全地移动到同步队
列。当节点移动到同步队列后,当前线程再使用LockSupport唤醒该节点的线程。
被唤醒后的线程,将从await()方法中的while循环中退出(isOnSyncQueue(Node node)方法

返回true,节点已经在同步队列中),进而调用同步器的acquireQueued()方法加入到获取同步状态的竞争中。(Queued自旋过程)

Condition的signalAll()方法,相当于对等待队列中的每个节点均执行一次signal()方法,效果就是将等待队列中所有节点全部移动到同步队列中,并唤醒每个节点的线程。

Java高并发编程(七):读写锁、LockSupport、Condition相关推荐

  1. Java Review - 并发编程_读写锁ReentrantReadWriteLock的原理源码剖析

    文章目录 ReentrantLock VS ReentrantReadWriteLock 类图结构 非公平的读写锁实现 写锁的获取与释放 void lock() void lockInterrupti ...

  2. Java高并发编程学习(三)java.util.concurrent包

    简介 我们已经学习了形成Java并发程序设计基础的底层构建块,但对于实际编程来说,应该尽可能远离底层结构.使用由并发处理的专业人士实现的较高层次的结构要方便得多.要安全得多.例如,对于许多线程问题,可 ...

  3. Java高并发编程详解系列-Java线程入门

    根据自己学的知识加上从各个网站上收集的资料分享一下关于java高并发编程的知识点.对于代码示例会以Maven工程的形式分享到个人的GitHub上面.   首先介绍一下这个系列的东西是什么,这个系列自己 ...

  4. Java高并发编程 (马士兵老师视频)笔记(一)同步器

    本篇主要总结同步器的相关例子:包括synchronized.volatile.原子变量类(AtomicXxx).CountDownLatch.ReentrantLock和ThreadLocal.还涉及 ...

  5. 29W 字总结阿里 Java 高并发编程:案例 + 源码 + 面试 + 系统架构设计

    下半年的跳槽季已经开始,好多同学已经拿到了不错的 Offer,同时还有一些同学对于 Java 高并发编程还缺少一些深入的理解,不过不用慌,今天老师分享的这份 27W 字的阿里巴巴 Java 高并发编程 ...

  6. Java高并发编程详解系列-7种单例模式

    引言 在之前的文章中从技术以及源代码的层面上分析了关于Java高并发的解决方式.这篇博客主要介绍关于单例设计模式.关于单例设计模式大家应该不会陌生,作为GoF23中设计模式中最为基础的设计模式,实现起 ...

  7. Java高并发编程:线程锁技术

    目录 1 什么是线程锁 2 synchronized 1. 对象锁 2. 修饰对象方法 3. 类锁 4. 对象锁和类锁 5. 卖火车票示例 6. 生产一个消费一个示例 3 Lock 3.1 重入锁 R ...

  8. @冰河老师的巨作,人手一册的Java高并发编程指南,值得了解一下

    还真不好意思,这次 Java Thread Pool 惊爆了! 高并发是每一个后端开发工程师都头疼的问题,也是工程师能力的分水岭.要想基于JDK核心技术,玩转高并发编程,必须要好好修炼内功才行. 文章 ...

  9. Java高并发编程:活跃性危险

    Java高并发程序中,不得不出现资源竞争以及一些其他严重的问题,比如死锁.线程饥饿.响应性问题和活锁问题.在安全性与活跃性之间通常存在依赖,我们使用加锁机制来确保线程安全,但是如果过度地使用加锁,则可 ...

最新文章

  1. pytorch错误解决: BrokenPipeError: [Errno 32] Broken pipe
  2. 核心员工要离职,怎么办?
  3. 怎样用命令行生成自己的签名文件keystore
  4. 肝了这套Python数据分析教程,进字节稳了!
  5. 语义分割中的类别不平衡的权重计算
  6. 键盘上的反引号怎么打
  7. 【LeetCode笔记】42. 接雨水(Java、动态规划)
  8. ESLint is disabled since its execution has not been approved or denied yet
  9. JNI开发笔记(三)--JNI工程的框架分析
  10. 页面传值,发生错误,如何传递中文信息
  11. sql 备份 文件大小_预测SQL备份大小
  12. Three20 NetWork
  13. IntelliJ IDEA 导入新项目以后的简单配置
  14. Java学习笔记之---集合
  15. Echarts中使用china.js
  16. 示波器电流探头传播延迟的测量
  17. 怎样在word中打印框选对√
  18. 玩转PS路径,轻松画logo!
  19. IntelliJ inspection gives “Cannot resolve symbol“ but still compiles code
  20. 前端页面中根据链接随机生成二维码

热门文章

  1. 认识微软Visual Studio Tools for AI
  2. c++中ifstream一次读取整个文件
  3. C++静态库与动态库(简介)
  4. 经验总结02-sql语句
  5. WinForm(C#)CheckedlistBox绑定数据,并获得选中的值(ValueMember)和显示文本(DisplayMember...
  6. 在WinForm中通过HTTP协议向服务器端上传文件(转)
  7. (转)SQL操作全集
  8. java从静态代理到动态代理的理解
  9. 国务院学位委员会关于授予具有研究生毕业同等学力人员硕士、博士学位的规定
  10. 谷歌解雇资深研究员Timnit Gebru 或仅因为一篇论文