JAVA AQS源码分析
转自: http://www.cnblogs.com/pfan8/p/5010526.html
JAVA AQS的全称为(AbstractQueuedSynchronizer),用于JAVA多线程的开发,从名称我们也可以看出,它实现了同步的队列,而这个队列是指线程队列。AQS类在java.util.concurrent.locks下面。
AQS和CAS作为JAVA5之后非常重要的特性,能在并发应用中提高程序性能,具体要就实际情况使用,因为JVM也在一直优化synchronized关键字,在JAVA7之后其性能也趋于稳定,不会随着线程数增加而导致性能骤降(具体可以取网上搜索对比数据)。
总之,一般情况下还是建议用synchronized
CAS(CompareAndSet)是最小粒度的操作,保证了原子性,通过硬件指令集实现。简单来说,CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则返回V。
基于此,我们才能完成非阻塞同步操作(当然还有一些其他原子命令,例如FAI,LL/SC等),目的就是用乐观锁来换取性能的提升。
为什么要说CAS呢?因为AQS也是基于CAS实现的,下面进入正题,我们通过源码来具体分析下AQS的实现:
-----------------------------------------------------------------------------华丽的分隔线---------------------------------------------------------------------------------------
AQS的包结构如下
继承的子类有
我们从ReentrantLock来分析AQS的实现原理:
ReentrantLock是一个可重入的排他锁。可重入指当前拥有锁的对象可重复进入同步区域,防止重复操作,例如网页登陆时重复点击按钮。排他锁就是指该锁(Lock)是互斥锁,只允许一个对象拥有锁。
先看看ReentrantLock的类结构
可以看到有3个内部类Sync,NonfairSync和FairSync。其中NonfairSync和FairSync均继承Sync,而Sync继承了AQS。NonfairSync和FairSync的区别在于当一个线程释放了锁的时候,队列里的其他线程是否按照FIFO的规则去获取锁的。
换句话说,FairSync能够保证先到的线程先拿到锁(有一个特殊情况,就是队列里的线程在unpark到获取锁的过程中有新的还未加入到队列中的线程获取到锁,不过这种情况发生的概率很小,基本不用考虑),而NonfairSync不保证
下面我们看看ReentrantLock是如何实现上锁的,这是lock函数:
public void lock() {
sync.lock();
}
sync的lock函数为抽象的,由子类实现,这里只给出FairSync的实现
final void lock() {
acquire(1);
}
acquire就是AQS提供的接口
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
这里涉及到4个新方法,让我们慢慢来分析:
首先,acquire有一个参数arg,是用于判断锁持有的次数,也就是重入的次数,当锁持有者需要释放锁的时候,则要将锁的state减去arg的值。在上面可以看到,调用acquire时传入的参数为1。
1. tryAcquire:AQS里tryAcquire的实现如下
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
明显是不对的,只抛出了一个异常,所以应该是子类覆盖了,那么看看FairSync的实现
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;
}
getState()是从ReentrantLock获取的状态,表示锁当前是否被持有,为0时表示没有线程持有锁。此时当前线程会去争取锁的持有权。
首先判断队列中是否有排在当前线程之前的线程,有的话放弃争抢锁。hasQueuedPredecessors是AQS里的方法:
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
tail记录队列里的尾节点,head就是头结点。这个函数就是判断当前线程是否是下一个可持锁的线程
然后,当当前线程满足条件时,就通过CAS设置c的值,同时通过AbstractOwnableSynchronizer类提供的setExclusiveOwnerThread接口将当前线程锁住,用于防止volatile字段被其他线程修改(这里是看注释后的个人理解)
如果c != 0,同时锁持有者为当前线程,那么这个请求就是重入请求了,将c+=acquire。如果上诉两个条件都不满足,返回false。
2. addWaiter:
private Node addWaiter(Node mode) {
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
根据注释可以知道,首先快速插入队列,其实就是判断队列是否为空,否则就要通过enq(node)将当前节点添加至队列中,为什么这样会慢些?我们看看enq函数
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;
}
}
}
}
这个方法可以好好看看:用for无限循环,考虑下多线程同时加入队列,通过CAS保证设置当前节点到尾节点。这里可能队列里的线程已经执行完任务释放锁了,所以还需要重新判断队列是否为空,因此其执行效率当然会有影响。
否则,设置前置节点,加入队列。用图例来帮助理解:
由于本身没有锁,可以有多个线程进来,如果有多个线程并发进入这个if判定区域,可能就会同时存在多个这样的数据结构,在各自形成数据结构后,多个线程都会去做compareAndSetHead(h)的动作,也就是尝试将这个临时h节点设置为head,
显然并发时只有一个线程会成功,因此成功的那个线程会执行tail = node的操作,整个AQS的链表就成为:
3. 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)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
同样是for无限循环,判断当前节点之前是否没有线程节点了,如果是,就去争抢锁。用上面给出的子类tryAcquire函数,成功的话设置相关参数,这里也解释了释放指针,帮助垃圾回收(GC)。
如果争抢失败,判断是否需要阻塞当前线程
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
waitState参数有以下几个:
- SIGNAL:当前线程持有锁
- CANCELLED:超时获取被中断
- CONDITION
- PROPAGATE
- 0:不属于以上任何一种情况
后面两个没怎么看,大致就是需要满足多个条件,PROPAGATE用于shared锁队列。
回到上面的方法:因为AQS支持中断等待,所以如果线程中断了争抢锁(CANCELLED),那么就不需要阻塞,直接返回。acquireQueued方法没有返回,而是设置一个interrupt参数为true而已,线程争抢锁失败的话继续休眠等待,而AQS里的doAcquireInterruptibly()发现争抢失败的话就直接throw new InterruptedException()。在ReentrantLock里需要调用ReentrantLock.lockInterruptibly()就会实现中断返回。否则AQS会尝试将当前线程状态设置成SIGNAL,失败就循环继续尝试
下面是parkAndCheckInterrupt
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
LockSupport.park通过Unsafe.park实现阻塞,它会设置一个AQS的blocker,让队列里的线程阻塞在一个地方,然后返回线程中断的判断
4. selfInterrupt
private static void selfInterrupt() {
Thread.currentThread().interrupt();
}
acquireQueued会返回boolean值表示线程是否中断,如果未中断,就调用Thread.interrupt()中断线程
---------------------------------------------------------------------------------------------------------------------
以上是锁的实现原理,当tryAcquire()成功之后,线程获取锁,执行任务,执行完毕之后,会调用AQS的release方法:
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
tryRelease是子类Sync的实现:
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
其他就不多说了,比较简单明晰。就看看unparkSuccessor怎么做的
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
compareAndSetWaitStatus(node, ws, 0); /*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next;
if (s == null || s.waitStatus > 0) {
s = null;
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);
}
通过CAS修改节点的waitStatus,然后将后续节点去除,这里会去遍历后续节点,判断其是否状态为CANCELLED,将所有非CANCELLED的节点唤醒。
JAVA AQS源码分析的更多相关文章
- ReentrantLock 与 AQS 源码分析
ReentrantLock 与 AQS 源码分析 1. 基本结构 重入锁 ReetrantLock,JDK 1.5新增的类,作用与synchronized关键字相当,但比synchronized ...
- 并发-AQS源码分析
AQS源码分析 参考: http://www.cnblogs.com/waterystone/p/4920797.html https://blog.csdn.net/fjse51/article/d ...
- Java Reference 源码分析
@(Java)[Reference] Java Reference 源码分析 Reference对象封装了其它对象的引用,可以和普通的对象一样操作,在一定的限制条件下,支持和垃圾收集器的交互.即可以使 ...
- Java 集合源码分析(一)HashMap
目录 Java 集合源码分析(一)HashMap 1. 概要 2. JDK 7 的 HashMap 3. JDK 1.8 的 HashMap 4. Hashtable 5. JDK 1.7 的 Con ...
- java集合源码分析(三):ArrayList
概述 在前文:java集合源码分析(二):List与AbstractList 和 java集合源码分析(一):Collection 与 AbstractCollection 中,我们大致了解了从 Co ...
- java集合源码分析(六):HashMap
概述 HashMap 是 Map 接口下一个线程不安全的,基于哈希表的实现类.由于他解决哈希冲突的方式是分离链表法,也就是拉链法,因此他的数据结构是数组+链表,在 JDK8 以后,当哈希冲突严重时,H ...
- AQS源码分析笔记
经过昨晚的培训.对AQS源码的理解有所加强,现在写个小笔记记录一下 同样,还是先写个测试代码,debug走一遍流程, 然后再总结一番即可. 测试代码 import java.util.concurre ...
- AQS源码分析看这一篇就够了
好了,我们来开始今天的内容,首先我们来看下AQS是什么,全称是 AbstractQueuedSynchronizer 翻译过来就是[抽象队列同步]对吧.通过名字我们也能看出这是个抽象类 而且里面定 ...
- Java集合源码分析(六)TreeSet<E>
TreeSet简介 TreeSet 是一个有序的集合,它的作用是提供有序的Set集合.它继承于AbstractSet抽象类,实现了NavigableSet<E>, Cloneable, j ...
随机推荐
- 希尔排序算法-python实现
#-*- coding: UTF-8 -*- import numpy as np def ShellSort(a): gap = a.size / 2 while gap >= 1: for ...
- 小米开源监控open-falcon
小米开源监控系统Open-Falcon安装使用笔记 07net01.com 发布于 2016-10-25 18:42:03 分类:IT技术 阅读(88) 评论 前言 近期爆出Zabbix有严重bug, ...
- vs2012,2013 update 离线下载(知识库)
由于微软提供的update是在线安装的. 加上layout参数可以全部下载完再安装. 命令行或批处理 VS2013.4.exe /layout
- 黄聪:WordPress 多站点建站教程(一):怎样开启WordPress多站点功能,实现手机移动端主题开发,与主站用户数据共享
为了开发手机移动端的wordpress,需要使用Wordpress的多站点功能. 1.打开WordPress根目录下的wp-config.php文件, 在文件的任何位置加上以下内容: define(' ...
- redis与lua
内容大纲 redis里使用eval和evalsha redis管理Lua脚本 php里使用redis的lua脚本 在redis里使用lua脚本的好处 1.Lua脚本在Redis中是原子执行的,执行过 ...
- Java 8 Lambda表达式之方法引用 ::双冒号操作符
双冒号运算符就是java中的方法引用,方法引用的格式是类名::方法名. 这里只是方法名,方法名的后面没有括号“()”.--------> 这样的式子并不代表一定会调用这个方法.这种式子一般是用作 ...
- sql之强制索引
1.今天我遇到一个问题,在处理百万级数据查询的时候,一般查询会很慢. 2.第一时间想到是建立联合索引,但是数据库存在多条索引的情况下,索引的执行是全部执行. 3.所以这里要按照特定的索引执行,就必须使 ...
- pythonNet08
线程通信 通信方法:多个线程共用进程空间,所以进程的全局变量对进程内线程均可见.线程往往使用全局变量进行通信 注意事项:线程间使用全局变量进行通信,全局变量为共享资源,往往需要同步互斥机制 线程的同步 ...
- Java HashMap 遍历方式探讨
JDK8之前,可以使用keySet或者entrySet来遍历HashMap,JDK8中引入了map.foreach来进行遍历. keySet其实是遍历了2次,一次是转为Iterator对象,另一次是从 ...
- MySQL my.cnf参数配置优化详解
[b]PS:本配置文件针对Dell R710,双至强E5620.16G内存的硬件配置.CentOS -100-300w的站点,主要使用InnoDB存储引擎.其他应用环境请根据实际情况来设置优化.[/b ...