迈向Java高级程序员(二)-----先从并发开始(AQS)

AQS 全称 AbstractQueuedSynchronizer (抽象的队列同步器)单看名字你可能不知道他干嘛的。但是如果告诉你ReentrantLock(可重入锁)就是基于它来实现的你可能就知道它的作用了。

先来看下AQS的类关系图:

AQS类关系图

AQS继承自 AbstractOwnableSynchronizer类,AbstractOwnableSynchronizer的实现其实非常简单,就是持有一个线程,提供了相关的getter/setter方法。仅此而已。

AbstractOwnableSynchronizer源码

AQS继承了exclusiveOwnerThread属性,表示独占锁的线程。

如果想很清楚的AQS是怎么实现同步的,找一个用它来实现锁功能的类来分析源码是再好不过了。我们就拿大名顶顶的的ReentrantLock(可重入锁)来配合分析AQS吧。

ReentrantLock(可重入锁)分公平锁和非公平锁。ReentrantLock 内部维护了两个内部类NonfairSync(非公平同步器)和FairSync(公平同步器)来实现公平锁和非公平锁。ReentrantLock默认情况下是非公平锁。我们先来看看公平锁是怎么实现的:

ReentrantLock的lock()方法直接调用相关同步器的lock()方法。FairSync(公平同步器)的源码很简单,我们来大致看下:

公平同步器FairSync源码

lock方法很简单,就是调用AQS的acquire()方法。acquire顾名思义获取。我们看看它到底是要获取什么。

AQS的acquire()

首先调用tryAcquire(1)一下,名字上就知道,这个只是试一试。因为有可能直接就成功了呢,也就不需要进队列排队了。所以它写的是!tryAcquire(1)。如果tryAcquire(1)没成功,那就说明要排队等锁啦,此时如果线程也排队成功了,那就把线程设置中断。同时返回给要锁的线程false,代表抢锁失败啦。

我们来详细分析下公平同步器FairSync的tryAcquire的实现。

1、getState()获取当前锁的状态,如果是0,则需要判断是不是队列中是不是有线程在等锁,为啥要这么判断一次,因为是公平锁啊。不能说你去请求锁的时候没有人占用锁,你就直接占了,因为可能有人已经排队了。如果队列中没有等待的线程,此时就各凭本事了,那就看谁单身的时间长了,此时就会用发起一个CAS尝试改变锁的state,如果操作成功了,说明抢锁成功,把锁的线程设置成自己,表明自己占有了锁。

2、如果锁已经被占用,那看看是不是自己占有的,因为是可重入锁吗。如果是自己占用的,就把状态继续加1。如果不是,那只能说明尝试抢锁失败了。此时又回重新进到AQS的实现里来进行排队操作。

对于高并发场景下,tryAcquire是一个乐观思想,但大多数情况下肯定还是会进入排队状态。那我们来看看AQS是怎么进行给线程排队的吧。

先大致说下有个概念我们再去看源码。

AQS内定义了如下几个变量来配合实现相关功能。

private transient volatile Node head;            持有当前锁的线程的节点

private transient volatile Node tail;                等待锁的线程的最后一个节点

private volatile int state;                                大于0代表锁有被线程占用

这里我们需要具体讲解下Node这个类,Node类是AQS的一个静态内部类。这个类是一个实现锁相关的辅助类。每个Node 实例都会持有一个线程的引用,每个Node可能会有一个前驱Node,和一个后继Node,如果Node没有前驱,它肯定会被AQS的head引用,如果一个Node没有后继,那他肯定会被tail所引用。

拿head这个Node来说吧,这个Node持有的线程会持有锁。当head持有的锁未被释放时,如果还有线程要抢占锁,那只能进入排队,会产生一个新的Node。你可以把Node理解为持有锁的线程的包装。每有一个线程要这个锁。就会形成一个Node,维护在锁内部进行排队。

排队的核心方法是addWaiter(Node mode),你以为它的参数Node就是直接加入一个节点进行排队吗。我告诉你它不是。JDK8以前可能是,但是JDK8不是,JDK8在Node内入引入了一个新变量Node nextWaiter;从官方的注释来看。这个对象是一个已有的Node,如果被传入一个Node中,则说明这两个Node可以共享锁。这个我们暂且不管,我们现在脑子只要记得要排队的Node是在addWaiter内部创建的就好了。此时应该搭配源码食用更佳:

为当前线程线程创建一个Node,判断队列的尾部是不是空,如果不是空就说明已经有线程在排队了,那就使用CAS把自己插到尾部。如果每成功,说明有别的线程比自己先排队了。执行enq方法。

addWaiter方法能看出,要排队的Node插入到尾部失败了,或者第一排队都会走到enq这个方法。这个方法分两种情况:

1、尾节点不是空,通过CAS把自己变成尾部节点。成功就直接返回啦,不成功通过自旋继续尝试直至成功。

2、尾节点是空,说明此时还没有人进入到排队队列中,此时头节点肯定也是空,别问我为啥。那此时就需要初始化一个头节点。另外自己不可能是头节点啦。上面已经说啦,head节点是为了对应持有锁的那个节点。但是head这个节点是第一个进入排队排队的线程负责创建的。但也只是尝试创建,因为也有可能别的线程现在抢着进入排队呢。不管是那个线程把head节点创建好了把,反正head节点有了,因为是刚创建的,所以头节点也是尾节点。此时尾节点也不为空啦,执行情况1.

就这样,AQS就帮你实现线程排队啦。

说完了加锁和排队,我们再来说说解锁:

ReentrantLock的解锁也很简单,也就是调用相关同步其的relese()方法。relese()方法也是AQS实现的,我们来具体看下:

和加锁一样的套路,先尝试解锁,如果没成功解锁,则返回false 。tryRelease()方法是个抽象方法,具体由子类实现。我们来看看ReentrantLock的同步器是怎么实现的:

1、判断要解锁的线程是不是当前持有锁的线程,如果不是抛异常。

2、对锁的状态减1,判断状态是不是0,为啥要判断为不为0,因为是重入锁吗,一个线程可能加锁多次呢。这也是为啥ReentrantLock容易造成死锁的原因,你加了多少此锁,你就要解除多少次,不然别的线程永远拿不到锁。

3、如果是完全解锁了,就把持有该锁的线程置为空。同时告诉AQS的release要干继续干事了。

尝试解锁成功后:

判断头节点是不是空,如果是空,直接返回了,如果不是空,则判断后继节点是不是在等待锁,如果再等待锁,执行unparkSuccessor()方法。

判断节点状态是不是小于0;为啥要这么判断,这里就要解释下Node的几个状态了:

1:代表此线程取消了争抢这个锁

-1:代表后续节点需要被唤醒:

-2:代表线程条件等待,满足条件才会抢锁

-3:读写锁中,当读锁最开始没有获取到操作权限,得到后会发起一个doReleaseShared()动作,内部也是一个循环,当判定后续的节点状态为0时,尝试通过CAS自旋方式将状态修改为这个状态,表示节点可以运行。

0:初始化状态,也代表正在尝试去获取临界资源的线程所对应的Node的状态

这样就可以这样解释了。

1、如果head节点当前waitStatus<0, 将其修改为0,表示要其他线程自己去竞争。

2、如果waitStatus>0唤醒后继节点,但是有可能后继节点取消了等待(waitStatus==1).从队尾往前找,找到waitStatus<=0的所有节点中排在最前面的.来唤醒线程。

执行完完成后,这时会返回到acquireQueued(final Node node, int arg)方法,而此方法 是自旋的,就是为了唤醒的线程重新去获取锁,直到异常退出或者执行完为止。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,293评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,604评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,958评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,729评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,719评论 5 366
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,630评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,000评论 3 397
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,665评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,909评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,646评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,726评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,400评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,986评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,959评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,197评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,996评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,481评论 2 342

推荐阅读更多精彩内容