《并发编程的艺术》阅读笔记之Lock与AQS
Lock接口
在jdk1.5之后,并发包下新增了一个lock接口,lock接口定义了锁的获取,锁的释放,等方法,需要用户手动设置。与关键字不同的是,lock具有可操作性,比如,可以中断线程,设置超时时间,如果时间截止,该线程还没有获得锁,就直接返回。同时lock接口有两个主要的实现类,可重入锁和读写锁。
java.util.concurrent.locks.Lock 接口,比 synchronized 提供更具拓展性的锁操作。它允许更灵活的结构,可以具有完全不同的性质,并且可以支持多个相关类的条件对象。
它的优势有(与synchronized区别):
- 可以使锁更公平。
- 可以使线程在等待锁的时候响应中断。
- 可以让线程尝试获取锁,并在无法获取锁的时候立即返回或者等待一段时间。
- 可以在不同的范围,以不同的顺序获取和释放锁。
Lock接口的实现(如Reentrantlock)基本都是通过聚合了一个同步器的子类来完成线程访问控制的。
队列同步器AbstractQueuedSynchronizer
AQS是用来构建锁或者其他同步组件的基础框架,它使用了一个int成员变量标识同步状态,通过内置的FIFO队列来完成资源获取线程的排队工作。同步器面向的是锁的实现者,它简化了锁的实现方式,屏蔽了同步状态管理、线程的排队、等待与唤醒的底层操作。
使用方法
同步器的主要是用方式是继承,子类通过继承同步器并实现它的抽象方法来管理同步状态,在实现抽象方法时使用同步器提供的3个同步状态获取和释放方法(getstate,setstate,compareAndSetState)对同步状态进行更改,他们可以保证状态的改变是安全的。子类推荐被定义为自定义同步组件的静态内部类。同步器自身没有实现任何同步接口,它仅仅是定义了若干同步状态获取和释放的方法来供自定义同步组件使用。
用户通过使用AQS提供的getState、setState、compareAndSetState三个访问/修改同步状态的方法实现TryAcqurie、tryRelease、tryAcquiredShared、tryReleaseShared、isHeldExclusively等5个方法来实现一个AQS子类即可。
然后在实现自定义同步组件时只需要通过将操作代理到我们自己定义的AQS子类上,调用同步器提供的acquire等模板方法(模板方法会相应调用用户实现的方法),即可实现一个可靠的自定义同步组件。(自己总结)
原理
AQS是列队同步器。他是并发类的基础组件。它的内部维护了一个volitale修饰的state状态变量。当状态为0时,说明没有任何线程占有锁,当状态改为1时,说明有线程持有锁。其他线程获取同步状态失败会被封装为一个Node节点的数据结构,然后加入一个FIFO的双向同步队列尾部,加入后以自旋的方式尝试获取同步状态(判断前驱结点为头结点且获取同步状态成功),如获取失败会阻塞当前线程。
//同步器的Acquire方法
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//同步器的addWaiter和enq方法
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode); // 快速尝试在尾部添加 Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node; return node;
}
}
enq(node);
return node;
}
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) {
// Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
//同步器的acquireQueued方法
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) { //唤醒后继续获取,前提是前驱结点必须是head
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())//获取失败则阻塞
interrupted = true;
}
} finally {
if (failed) cancelAcquire(node);
}
}
而被阻塞线程的唤醒主要依靠前驱节点的出队和阻塞线程的中断实现。当成功获取到同步状态的线程执行完业务逻辑,同步器调用tryRelease释放同步状态,之后唤醒(用的实际是LockSupport)头结点的后继结点中的线程。tryRelease不需要循环cas保证线程安全因为只会有一个线程持有同步状态。被唤醒的线程会把自己设置为新的头结点。
同步器拥有首节点和尾结点,线程被转换成节点加入队列(尾部)这个过程需要保证线程安全,因此同步器提供了一个基于CAS的设置尾结点的方法comPareandSetTail。
其中内部类还有一个conditionObject构建等待队列,将condition调用await方法时线程会加入到等待队列中,直到condition调用signal()方法后,线程会被唤醒从等待队列进入同步队列中进行锁的竞争。
以上所说为独占式获取和释放,共享式获取的逻辑和上面差不多,调用tryAcquiredShared方法返回值>=0则能够获取同步状态,获取失败则转换为节点进入等待队列尾部,加入后以自旋的方式尝试获取同步状态(判断前驱结点为头结点且获取同步状态成功),如获取失败会阻塞当前线程。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
共享式中当有获取到同步状态的线程释放同步状态时,将会唤醒后续处于等待状态的节点。这块跟独占式的区别主要在于tryReleaseShared方法必须通过循环CAS确保同步状态线程安全释放,因为释放同步状态的操作会来自多个线程。
ReentrantLock
ReentrantLock,可重入锁,是一种递归无阻塞的同步机制。它可以等同于 synchronized 的使用,但是 ReentrantLock 提供了比 synchronized 更强大、灵活的锁机制,可以减少死锁发生的概率。
ReentrantLock有公平锁和非公平锁两种实现方式。
非公平锁
//ReentrantLock的nonfairTryAcquire方法
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
CAS获取同步状态是常规操作,为了让当前持有锁的线程再次获取锁不被阻塞,ReentrantLock在实现nonfairTryAcquire时判断线程是否持有锁,持有的话就增加state值,并return true。
//ReentrantLock的tryRelease方法
protected final boolean tryRelease(int releases) {
int c = getState() - releases; //因为只有一个线程持有锁,所以不需要用CAS
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
释放的时候判断是否state变为了0,只有为0才返回true,否则false。
公平锁
//ReentrantLock的tryAcquire方法
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
} else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
唯一得区别是公平锁调用 #hasQueuedPredecessors() 方法检查是否有前序节点,有则返回false。
非公平锁和公平锁获取同步状态的过程,会发现两者唯一的区别就在于,公平锁在获取同步状态时多了一个限制条件 <1> 处的 #hasQueuedPredecessors() 方法,是否有前序节点,即自己不是首个等待获取同步状态的节点。如果是则返回 true ,否则返回 false 。
读写锁
java并发包提供的读写锁实现为ReentrantReamWriteLock
读写状态设计
读写锁同样依赖自定义同步器来实现同步功能,而读写状态就是其同步器的同步状态。回想ReentrantLock中自定义同步器的实现,同步状态表示锁被一个线程重复获取的次数,而读 写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状 态,使得该状态的设计成为读写锁实现的关键。
如果在一个整型变量上维护多种状态,就一定需要“按位切割使用”这个变量,读写锁将 变量切分成了两个部分,高16位表示读,低16位表示写,划分方式如下图所示。
当前同步状态表示一个线程已经获取了写锁,且重进入了两次,同时也连续获取了两次 读锁。
读写锁是如何迅速确定读和写各自的状态呢?
答案是通过位运算。假设当前同步状态 值为S
写状态等于S&0x0000FFFF(将高16位全部抹去),
读状态等于S>>>16(无符号补0右移 16位)。当写状态增加1时,等于S+1,当读状态增加1时,等于S+(1<<16),也就是 S+0x00010000。
根据状态的划分能得出一个推论:S不等于0时,当写状态(S&0x0000FFFF)等于0时,则读 状态(S>>>16)大于0,即读锁已被获取。这一点在源码中用于获取写锁时判断读锁。
写锁的获取与释放
//ReentrantReadWriteLock的tryAcquire方法
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState(); //状态值
int w = exclusiveCount(c);
if (c != 0) { // 存在读锁或者当前获取线程不是已经获取写锁的线程,即上文说的推论
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
setState(c + acquires);
return true;
}
if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) {
return false;
}
setExclusiveOwnerThread(current);
return true;
}
写锁的获取:
写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程, 则当前线程进入等待状态。
写锁的释放:
写锁的释放与ReentrantLock的释放过程基本类似,每次释放均减少写状态即可。当写状态为0 时表示写锁已被释放,从而等待的读写线程能够继续访问读写锁,同时前次写线程的修改对后续读写线程可见。
读锁的获取与释放
读锁是一个支持重进入的共享锁,它能够被多个线程同时获取。
//ReentrantReadWriteLock的tryAcquireShared方法
protected final int tryAcquireShared(int unused) {
for (;;) {
int c = getState();
int nextc = c + (1 << 16);
if (nextc < c)
throw new Error("Maximum lock count exceeded");
if (exclusiveCount(c) != 0 && owner != Thread.currentThread())
return -1;
if (compareAndSetState(c, nextc))
return 1;
}
}
读锁的获取:
在tryAcquireShared(int unused)方法中,如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。如果当前线程获取了写锁或者写锁未被获取,则当前线程(线程安全,依靠CAS保证)增加读状态,成功获取读锁。(总结,只要其他线程没有获取到写锁,并且读锁数量没超出最大值就行)
读锁的释放:
读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的 值是(1<<16)。
锁降级
锁降级指的是写锁降级成为读锁。但是锁降级要求在先不释放写锁的前提下获取读锁,然后再释放写锁。这样做的目的在于保证当前线程在降级到读锁之前不会有其他线程修改数据(如果先释放写锁再获取读锁,在当前线程释放写锁之后获取到读锁之前有可能其他线程获取到写锁修改了数据,那样当前线程将无法感知到数据修改,从而造成问题)。
备注:RentrantReadWriteLock不支持锁升级(把持读锁、获取写锁,最后释放读锁的过程)。目的 也是保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了 数据,则其更新对其他获取到读锁的线程是不可见的(这里的不可见我的理解是不可感知)。评注:我觉得这个锁升级完全就不合理呀,好几个线程都拿着读锁,按照上面写锁获取tryAcquire的逻辑存在读锁也不能获取到写锁啊。
Condition
用途
Condition接口也提供了Object类似的监视器方法,与Lock配合可以实现等待/通知模式,Condition方法使用的前置条件为获取锁。
与synchronized的等待唤醒机制相比Condition具有更多的灵活性以及精确性,这是因为notify()在唤醒线程时是随机(同一个锁),而Condition则可通过多个Condition实例对象建立更加精细的线程控制,也就带来了更多灵活性了,我们可以简单理解为以下两点。
通过Condition能够精细的控制多线程的休眠与唤醒。
对于一个锁,我们可以为多个线程间建立不同的Condition。线程之间的通信(等待唤醒机制,类似wait/notify)。
Object监视器模型上,一个对象拥有一个同步队列和等待队列,而并发包中的Lock(更确切地说是同步器)拥有一个同步队列和多个等待队列。
实现
ConstionObject是AQS的内部类,每个condition对象都包含一个队列(等待队列),Condition拥有首节点(firstWaiter)和尾节点 (lastWaiter)。Condition定义了等待/通知两种类型的方法,当前线程调用这些方法时,需要提前获取到 Condition对象关联的锁。一个Condition包含一个等待队列。
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)) { //await调用后和signal调用后从这被唤醒
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0) break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null)
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
Await方法其实做了三件事
- 是将当前线程封装node节点加入到等待队列中,
- 是释放同步状态并且唤醒(同步队列的)后继节点的线程,
- 是循环判断当前线程是否在同步队列中。如果没有就挂起当前线程,从而当前线程进入等待状态。(这步说的有点乱,其实是加入等待队列后这个判读条件肯定会false,然后就阻塞当前线程进入等待状态)因为等待线程被唤醒时,会从等待队列加入到同步队列中。
public final void signal() {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
if (first != null)
doSignal(first); //只需要持有锁的线程将等待队列中的首节点移入同步队里然后唤醒,之后被唤醒几点会从上文的await方法中换新,执行acquireQueued在其中循环竞争,代码非常巧妙
}
Signal方法主要做了两件事
是判断当前线程是否持有锁(独占锁),如果没有就抛出异常。因为独占模式才会放入等待队列中。
获取等待队列中的首节点,将其移动到同步队列并使用LockSupport唤醒该节点中的线程
被唤醒的线程将从await()方法中的while循环中退出(即上面说到的await第三件事),进而调用同步器的acquireQueued()方法加入到获取同步状态的竞争中。
condition的signalAll()方法相当于对等待队列中的每个节点均执行一次signal()方法,效果就是将等待队列中的所有节点全部移动到同步队列中,并唤醒每个节点的线程。
《并发编程的艺术》阅读笔记之Lock与AQS的更多相关文章
- <<Java并发编程的艺术>>-阅读笔记和思维导图
最近在坚持每天阅读<>,不但做好笔记(MarkDown格式),还做好思维导图. 如果大家感兴趣,可以可以到码云上阅读笔记和到ProcessOn上阅读思维导图. 码云:https://git ...
- Java并发编程的艺术读书笔记(1)-并发编程的挑战
title: Java并发编程的艺术读书笔记(1)-并发编程的挑战 date: 2017-05-03 23:28:45 tags: ['多线程','并发'] categories: 读书笔记 --- ...
- Java并发编程的艺术读书笔记(2)-并发编程模型
title: Java并发编程的艺术读书笔记(2)-并发编程模型 date: 2017-05-05 23:37:20 tags: ['多线程','并发'] categories: 读书笔记 --- 1 ...
- 《Java并发编程的艺术》笔记
第1章 并发编程的挑战 1.1 上下文切换 CPU通过时间片分配算法来循环执行任务,任务从保存到再加载的过程就是一次上下文切换. 减少上下文切换的方法有4种:无锁并发编程.CAS算法.使用最少线程.使 ...
- java并发编程的艺术(三)---lock源码
本文来源于翁舒航的博客,点击即可跳转原文观看!!!(被转载或者拷贝走的内容可能缺失图片.视频等原文的内容) 若网站将链接屏蔽,可直接拷贝原文链接到地址栏跳转观看,原文链接:https://www.cn ...
- synchronized的实现原理-java并发编程的艺术读书笔记
1.synchronized实现同步的基础 Java中的每个对象都是可以作为锁,具体有3种表现. 1.对于普通同步方法,锁是当前实例对象. 2.对于静态同步方法,锁是当前类的Class对象. 3.对于 ...
- 《并发编程的艺术》阅读笔记之Sychronized
概述 在JDK1.6中,锁一共四种状态,级别由低到高依次是:无锁状态.偏向锁状态.轻量级锁状态和重量级锁状态.锁可以升级但不能降级,这是为了提高获得锁和释放锁的效率.只有重量级锁涉及到操作系统线程切换 ...
- 读《Java并发编程的艺术》学习笔记(一)
接下来一个系列,是关于<Java并发编程的艺术>这本书的读书笔记以及相关知识点,主要是为了方便日后多次复习和防止忘记.废话不多说,直接步入主题: 第1章 并发编程的挑战 并发编程的目的是 ...
- 《Java并发编程的艺术》读书笔记:二、Java并发机制的底层实现原理
二.Java并发机制底层实现原理 这里是我的<Java并发编程的艺术>读书笔记的第二篇,对前文有兴趣的朋友可以去这里看第一篇:一.并发编程的目的与挑战 有兴趣讨论的朋友可以给我留言! 1. ...
随机推荐
- Python Seaborn综合指南,成为数据可视化专家
概述 Seaborn是Python流行的数据可视化库 Seaborn结合了美学和技术,这是数据科学项目中的两个关键要素 了解其Seaborn作原理以及使用它生成的不同的图表 介绍 一个精心设计的可视化 ...
- Jenkins打造多分支流水线指南
overview: 多分支工作流程带来了以下几个关键能力: 在代码仓库中,每个新分支都有自己单独的工作流水线(job). 每个工作流水线都记录了对应分支的构建和变更历史. 可以自定义设置流水线随着分支 ...
- Hbase 整合 Hadoop 的数据迁移
上篇文章说了 Hbase 的基础架构,都是比较理论的知识,最近我也一直在搞 Hbase 的数据迁移, 今天就来一篇实战型的,把最近一段时间的 Hbase 整合 Hadoop 的基础知识在梳理一遍,毕竟 ...
- python框架-Django安装使用
1.安装pip sudo apt-get install python-pip 遇到问题需要更新下语言包 sudo apt-get update 检查pip是否安装成功 pip -V 查看已安装包 p ...
- mac 中使用git 和pycharm提交项目
一.安装Git 1.验证git是否安装: 终端中输入: git 如果安装过出现: 2.安装git: 进入https://git-scm.com: 点击 Download 2.23.0 for Mac ...
- Java工程师技能点梳理
从个人技术积累的角度,来看看一名合格的Java工程师在面试时所需要的知识技能. 1.基本语法 这包括static.final.transient等关键字的作用,foreach循环的原理等等.今天面试我 ...
- 基于MVP模式实现四则运算器
基于MVP模式四则运算器 来到新东家,项目的框架采用的是MVP模式,刚来公司的时候,项目经理给予分配小任务,首先熟悉MVP模式,而后普通的四则运算器的实现使用MVP分层.这里主要回顾当时做任务时候的对 ...
- Python中矩阵的完全显示问题以及输出矩阵中的非零元问题
问题:有时需要查看矩阵的所有元素,但矩阵过大时中间部分会用[... ...]号代替,这样不方便数据分析. 解决: # 解决不完全显示问题 import numpy as np np.set_print ...
- C++STL(二)——vector容器
STL--vector容器 vector对象的概念 vector基本操作 vector对象的初始化.赋值 vector查找.替换(已在上一片 string类 博客总结过了,不再总结) vector添加 ...
- 如何将 .NetFramework WebApi 按业务拆分成多个模块
在 .NetFramework 中使用 WebApi ,在不讨论 微服务 的模式下,大部分都是以层来拆分库的 : 基础设施 数据存储层 服务层 WeApi 层 一些其它的功能库 项目结构可能会像下面这 ...