Java并发编程总结3——AQS、ReentrantLock、ReentrantReadWriteLock
本文内容主要总结自《Java并发编程的艺术》第5章——Java中的锁。
一、AQS
AbstractQueuedSynchronizer(简称AQS),队列同步器,是用来构建锁或者其他同步组建的基础框架。该类主要包括:
1、模式,分为共享和独占。
2、volatile int state,用来表示锁的状态。
3、FIFO双向队列,用来维护等待获取锁的线程。
AQS部分代码及说明如下:
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable { static final class Node {
/** 共享模式,表示可以多个线程获取锁,比如读写锁中的读锁 */
static final Node SHARED = new Node();
/** 独占模式,表示同一时刻只能一个线程获取锁,比如读写锁中的写锁 */
static final Node EXCLUSIVE = null; volatile Node prev;
volatile Node next;
volatile Thread thread;
} /** AQS类内部维护一个FIFO的双向队列,负责同步状态的管理,当前线程获取同步状态失败时,同步器会将当前线程以及等待状态等
构造成一个节点Node并加入同步队列;当同步状态释放时,会把首节点中线程唤醒,使其再次尝试同步状态 */
private transient volatile Node head;
private transient volatile Node tail; /** 状态,主要用来确定lock是否已经被占用;在ReentrantLock中,state=0表示锁空闲,>0表示锁已被占用;可以自定义,改写tryAcquire(int acquires)等方法即可 */
private volatile int state;
}
这里主要说明下双向队列,通过查看源码分析,队列是这个样子的:
head -> node1 -> node2 -> node3(tail)
注意:head初始时是一个空节点(所谓的空节点意思是节点中没有具体的线程信息),之后表示的是获取了锁的节点。因此实际上head->next(即node1)才是同步队列中第一个可用节点。
AQS的设计基于模版方法模式,使用者通过继承AQS类并重写指定的方法,可以实现不同功能的锁。可重写的方法主要包括:

二、通过ReentrantLock学习AQS的使用
1、公平锁的获取
/**
* Sync object for fair locks
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L; final void lock() {
acquire(1);
} /**
* 首先尝试获取锁,如果tryAcquire(arg)返回true,获取锁成功;
* 如果失败,则调用acquireQueued(addWaiter(Node.EXCLUSIVE), arg),将当前线程封装成Node节点加入到同步队列队尾,之后阻塞当前线程
*/
public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
} /**
* 获取state的值,如果等于0表示锁空闲,可以尝试获取;
* 查看当前线程是否是FIFO队列中的第一个可用节点,如果是第一个,则尝试通过CAS方式获取锁, 这保证了等待时间最长的必定先获取锁
*/
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;
}
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
/**
* 如果发现当前节点的前一个节点为head,那么尝试获取锁,成功之后删除head节点并将自己设置为head,退出循环;
* 如果当前节点为阻塞状态,需要unpark()唤醒,release()方法会执行唤醒操作
*/
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
/**
* 为了避免无意义的自旋,同步队列中的线程会通过park(this)方法用于阻塞当前线程
*/
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
}
2、公平锁的释放
更新状态值state,之后唤醒同步队列中的第一个等待节点,unparkSuccessor(Node node)。
三、公平锁和非公平锁
ReentrantLock默认的锁为非公平锁,其主要原因在于:与公平锁相比,可以避免大量的线程切换,极大的提高性能。
先看一个非公平锁的例子:
public class AQS2 {
private ReentrantLock lock = new ReentrantLock(false);
private Thread[] threads = new Thread[3];
public AQS2() {
for (int i = 0; i < 3 ; i++) {
threads[i] = new Thread(new Runnable() {
public void run() {
for (int i = 0; i < 2; i++) {
try {
lock.lock();
Thread.sleep(100);
System.out.println(Thread.currentThread().getName());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
});
}
}
public void startThreads() {
for (Thread thread : threads) {
thread.start();
}
}
public static void main(String[] args) {
AQS2 aqs2 = new AQS2();
aqs2.startThreads();
}
}
运行结果为:

这段代码(每个线程2次获取锁/释放锁)的运行结果我一开始没有想清楚,之前我是这么想的:
Thread0先获取锁,之后sleep 100ms,那么等待获取锁的同步队列为:
head -> thread1 -> thread2 -> thread0 -> thread1 -> thread2。
从运行结果可知,第二次获取锁的还是thread0,但是锁的释放release(int args)却总是从同步队列的第一个可用节点开始,那就把thread1从队列中移除了,逻辑明显不对了。
后来重新看了代码,比较了非公平锁和公平锁之间的不同时,才终于明白。
非公平锁获取锁最大的不一样的地方在于:线程可以无视sync同步队列插队!一旦插队成功,获得了锁,那么该线程当然也就不用在排队了。所以以上程序的同步队列应该为:
head -> thread1 -> thread2。
非公平锁源代码主要的不同点有2点:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
//不同点1
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) { //不同点2
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
thread0第一次释放锁之后,会立刻通过lock.lock()操作继续尝试获取锁。非公平锁的lock()方法会直接尝试获取锁,无视同步队列,因此很大概率会再次获得锁;如果失败了,那么执行nonfairTryAcquire(int acquires)方法,该方法和tryAcquire(int acquires)最大的不同在于,缺少了hasQueuedPredecessors()的判断,即不需要判断当前线程是否是同步队列的第一个可用节点,甚至也不需要判断当前线程是否在同步队列中,直接尝试获取锁即可。
四、ReentrantReadWriteLnock
理解了AQS的原理后,读写锁也就不难理解了。读写锁分为2个锁,读锁和写锁。读锁在同一时刻允许多个线程访问,通过改写int tryAcquireShared(int arg)以及boolean tryReleaseShared(int arg)方法即可;写锁为独占锁,通过改写boolean tryAcquire(int arg)以及boolean tryRelease(int arg)方法即可。
由于AQS中只提供了一个int state来表示锁的状态,那么如何表示读和写2个锁呢?解决办法是前16位表示读锁,后16位表示写锁。由于锁的状态只有16位,因此无论是对于读锁或者是写锁,其state最大值均为65535,即所有获得了锁的线程的拿到锁的总次数(由于是重进入锁,因此每个线程可以拿到n个锁)不超过65536。由于读写锁主要的应用场景为多读少写,所以如果感觉读锁的65535不够用,可以自己改写读写锁即可,比如分配int state的前24位为读锁,后8位为写锁。
读写锁还提供了一些新的方法,比如final int getReadHoldCount(),返回当前线程获取读锁的次数。由于读状态保存的是所有获取读锁的线程读锁次数的总和,因此每个线程自己的读锁次数需要单独保存,引入了ThreadLocal,由线程自身维护。
Java并发编程总结3——AQS、ReentrantLock、ReentrantReadWriteLock的更多相关文章
- Java并发编程锁系列之ReentrantLock对象总结
Java并发编程锁系列之ReentrantLock对象总结 在Java并发编程中,根据不同维度来区分锁的话,锁可以分为十五种.ReentranckLock就是其中的多个分类. 本文主要内容:重入锁理解 ...
- 【Java并发编程实战】----- AQS(三):阻塞、唤醒:LockSupport
在上篇博客([Java并发编程实战]----- AQS(二):获取锁.释放锁)中提到,当一个线程加入到CLH队列中时,如果不是头节点是需要判断该节点是否需要挂起:在释放锁后,需要唤醒该线程的继任节点 ...
- Java并发编程总结3——AQS、ReentrantLock、ReentrantReadWriteLock(转)
本文内容主要总结自<Java并发编程的艺术>第5章——Java中的锁. 一.AQS AbstractQueuedSynchronizer(简称AQS),队列同步器,是用来构建锁或者其他同步 ...
- Java并发编程-看懂AQS的前世今生
在具备了volatile.CAS和模板方法设计模式的知识之后,我们可以来深入学习下AbstractQueuedSynchronizer(AQS),本文主要想从AQS的产生背景.设计和结构.源代码实现及 ...
- 【Java并发编程实战】----- AQS(二):获取锁、释放锁
上篇博客稍微介绍了一下AQS,下面我们来关注下AQS的所获取和锁释放. AQS锁获取 AQS包含如下几个方法: acquire(int arg):以独占模式获取对象,忽略中断. acquireInte ...
- Java并发编程:用AQS写一把可重入锁
Java并发编程:自己动手写一把可重入锁详述了如何用synchronized同步的方式来实现一把可重入锁,今天我们来效仿ReentrantLock类用AQS来改写一下这把锁.要想使用AQS为我们服务, ...
- JAVA并发编程: CAS和AQS
版权声明:本文为博主原创文章,转载请注明出处 https://blog.csdn.net/u010862794/article/details/72892300 说起JAVA并发编程,就不得不聊 ...
- 【Java并发编程实战】----- AQS(四):CLH同步队列
在[Java并发编程实战]-–"J.U.C":CLH队列锁提过,AQS里面的CLH队列是CLH同步锁的一种变形.其主要从两方面进行了改造:节点的结构与节点等待机制.在结构上引入了头 ...
- 【Java并发编程实战】—– AQS(四):CLH同步队列
在[Java并发编程实战]-–"J.U.C":CLH队列锁提过,AQS里面的CLH队列是CLH同步锁的一种变形. 其主要从双方面进行了改造:节点的结构与节点等待机制.在结构上引入了 ...
随机推荐
- App上架流程[利用Archive进行上传]
作者 M_Lee2016.01.22 10:47 写了14852字,被32人关注,获得了49个喜欢 [iOS]App上架流程[利用Archive进行上传] 字数2186 阅读507 评论3 喜欢9 今 ...
- Android——编译release版签名系统
http://blog.csdn.net/jscese/article/details/24243171 在我的第一篇博客里面http://blog.csdn.net/jscese/article/d ...
- 第一个C#语言
第一个C#程序 .NET和C#的区别 1. C#只是.NET的一部分,.NET不止包含C# 2. C#是一种程序语言,.NET是一个平台.框架 IDE环境:vs 2012 VS2012的窗口结构 ...
- Python中subprocess 模块 创建并运行一个进程
python的subprocess模块,看到官方声明里说要尽力避免使用shell=True这个参数,于是测试了一下: from subprocess import call import shlex ...
- 2019.01.16 bzoj4399: 魔法少女LJJ(线段树合并)
传送门 线段树合并菜题(然而findfindfind函数写错位置调了好久) 支持的操作题目写的很清楚了,然后有一个神奇的限制c≤7c\le7c≤7要注意到不然会去想毒瘤线段树的做法. 思路: 这题只有 ...
- JAVA遇上HTML-----JSP 篇基本概念
Java Web简介 1.什么是WEB应用程序: Web应用程序是一种可以通过Web访问的应用程序.Web应用程序的一个最大好处是用户很容易访问应用程序.用户只需要有浏览器即可,不需要再安装其他软件. ...
- 安装kafka 集群 步骤
1.下载 http://mirror.bit.edu.cn/apache/kafka/2.1.0/kafka_2.11-2.1.0.tgz 2.解压 tar -zxvf kafka_2.11-2.1 ...
- git——^和~的区别(转)
原文地址: http://www.cnblogs.com/softidea/p/4967607.html 一. 引子 在git操作中,我们可以使用checkout命令检出某个状态下文件,也可以使用re ...
- SQL优化实战之加索引
有朋友和我说他的虚机里面的mysql无法跑sql,但是在本地环境是这个sql是可以跑出来的.碰到这个问题第一反应是:死锁. 于是让他查询数据库的几个状态: 发现连即时锁都非常少,不是锁的问题. 进一步 ...
- 初识Android的ReactiveX
初识Android的ReactiveX 开发一个复杂一点的Android应用都会用到网络请求,交互和动画.这些都意味着 要写很多的回调嵌套.这样的代码也被称为callback hell(回调地狱).这 ...