Java中的锁系列3

上一章我们讲了synchronized对象锁的各种级别。但是,有的时候会被人问到ReentrantLock是怎么实现的么?AQS是啥?公平锁和非公平锁怎么实现的么?排他锁和共享锁怎么实现么?

就算我们弄懂了前两章的内容,还是只能不好意思的回答:不会。

相关的类

在文章的开头,我自己画了一个相关的类的类图,让你可以在开始的时候有个大概的了解。在读文章的过程中,你也可以时不时回来看一下这个图,应该会有很大的收获。


ReentrantLock

首先我们从ReentrantLock的构造函数入手,来看下ReentrantLock是怎么实现的。


ReentrantLock有两种实现,FairSync公平锁的实现和NonfairSync非公平锁的实现。

ReentrantLock的lock和unlock也是调用的对应sync里的lock和unlock。

FairSync和NonfairSync都继承了Sync类,而Sync则继承了AbstractQueuedSynchronizer, AbstractQueuedSynchronizer也就是传说中的AQS,AQS继承了AbstractOwnableSynchronizer,我们可以把AbstractOwnableSynchronizer叫AOS(这个名字是我自己取的)。

NonfairSync

NonfairSync的代码特别少,只有两个方法lock()和tryAcquire()。


我看到这个代码第一映像是:不是应该lock()跟unlock()方法么,怎么会是lock()和tryAcquire()呢?

实际上,我们看下ReentrantLock的lock()方法和unlock()方法:



也就是说lock调的是NonfairSync的lock()方法,unlock调用的应该是NonfairSync的release()方法。

relase()

不管NonfairSync还是FairSync,release()方法都是在他们的父类AQS里实现的。这意味着,公平锁和非公平锁,释放锁的步骤是一样的。

下面是AQS里的release的代码。


这个就解释了为什么NonfairSync和FairSync里只有lock了。


lock()

NonfairSync里的lock()我们知道是干什么的了,那么tryAcquire()又是个什么鬼呢?

NonfairSync的实现里tryAcquire()只有一行,return nonfairTryAcquire(acquires)。

我们再来看下nonfairTryAcquire方法。


通过读nonfairTryAcquire()这个方法,我们知道,实际上这个方法就是试着通过state和对应的thread值去获取锁,获取成功了返回true,失败了返回false。

如果当前线程已经获取到锁了,那么再次调用lock的时候,nextc实际上是+1,然后将nextc设置到state里去。这样也实现了ReentrantLock的可重入性。

但是,问题来了,tryAcquire这个方法在哪里调用呢?

我们再回过头来看一下lock方法


compareAndSetState()方法就是对state的一个CAS操作,期望值0,修改值1。成功了,就意味着拿到锁了。

acquire()方法,是在AQS里实现的。我们看下acquire的代码。


好了,找到tryAcquire()的调用地方了:实际上在lock里调用的。只不过tryAcquire在AQS里是一个虚方法。真正实现其实还是在子类也就是NonfairSync和FairSync。

FairSync

我们在来看一下FairSync类的实现。


同样很简单,跟NonfairSync的区别, 无非就是lock方法少了一次CAS操作,然后tryAcquire方法多了一个hasQueuedPredecessors()的判断。

这意味着hasQueuedPredecessors()的判断,很可能是用来保证锁的公平性的。

锁的公平实际上就是指先进等待队列的先拿到锁。我们从hasQueuedPredecessors的方法名字中其实就可以看出,这个方法就是为了保证当前获取锁的线程前面没有其他等待获取锁的线程了,如果有其他线程在前面,当前线程就拿不到锁。这样也确实是可以保证得到锁的顺序为先来先得,也就是保证锁的公平性。

到这里对NonfairSync和FairSync的分析就告一段落了。lock已经转移到了对AQS里acquire方法的分析,unlock已经转移到了对AQS里release方法的分析。

AQS

AbstractQueuedSynchronizer,传说中的AQS。

AQS其实是一种变种的CLH锁。关于CLH的资料可以看这个blog:

http://blog.csdn.net/aesop_wubo/article/details/7533186

前面我们说到了lock()已经转移到了AQS里的acquire()方法。那我们就从acquire()作为入口开始了解AQS。

Acquire获取锁的流程如下:


我们在来看下acquire()的代码:


首先通过tryAcquire()尝试获取锁,如果获取不到,就创建一个代表当前线程的节点加入到等待队列的尾部,这个节点的模式为EXCLUSIVE也就是排他模式,对应的还有一个模式是SHARE,也就是共享模式。

tryAcquire()方法我们前面已经讲过了,下面主要看一下addWaiter()方法和acquireQueued()方法。

addWaiter

通过addWaiter()的方法名,我们就可以知道这个方法是往等待队列里加入一个Node。


这个方法应该分为两部分来读:

tail != null和tail == null。

tail != null表示当前队列里有元素,那么就设置当前节点为队尾节点。

tail== null表示当前队列里没有元素,那么就执行enq()方法,将这个节点入队。

其实我也疑惑,这里直接调enq方法不就行了吗?为什么还要进行一次判断呢?希望有大神能给解释下~

我们再看下enq()方法


enq()方法采用的就是变种CLH算法,先看尾结点是否为空,如果为空就创建一个傀儡结点,头尾指针都指向这个傀儡结点,这一步只会在队列初始化时会执行;

如果尾结点非空,就采用CAS操作将当前结点插入到尾结点后面,如果在插入的时候尾结点有变化,就将尾结点向后移动直到移动到最后一个结点为止,然后再把当前结点插入到尾结点后面,尾指针指向当前结点,入队成功。

acquireQueued

我们再来看一下acquireQueued()方法。


将新加入的结点放入队列之后,这个结点有两种状态,要么获取锁,要么就挂起。

如果这个节点的前趋节点是头节点,就会用tryAcquire()来尝试获取锁。如果成功,就将当前节点设置为头节点,并且将前趋节点设置为null,这样方便前趋节点被GC回收。如果失败就继续在for循环里获取,直到获取成功。

如果这个结点不是头结点,就看看这个结点是否应该挂起,如果应该挂起,就挂起当前结点,是否应该挂起是通过shouldParkAfterFailedAcquire()方法来判断的。


看到这里的Node.SIGNAL,可能很多人也疑惑了,这些状态都代表什么意思呢?

CLH锁的本质是一个FIFO的队列,既然是队列, 那肯定有一个个的节点。

CLH锁的节点里,只有true和false的状态。但是AQS的节点有很多。

具体的状态值如下:

static final int CANCELLED =1;

static final int SIGNAL= -1;

static final int CONDITION = -2;

static final int PROPAGATE = -3;

下面解释下这些节点状态的具体含义:

CANCELLED:因为超时或者中断,结点会被设置为取消状态,被取消状态的结点不应该去竞争锁,只能保持取消状态不变,不能转换为其他状态。处于这种状态的结点会被踢出队列,被GC回收;

SIGNAL:表示这个结点的继任结点被阻塞了,到时需要通知它;

CONDITION:表示这个结点在条件队列中,因为等待某个条件而被阻塞;

PROPAGATE:使用在共享模式头结点有可能牌处于这种状态,表示锁的下一次获取可以无条件传播;

0:None of the above,新结点会处于这种状态。

了解了状态,我们再来看看这个方法。

该方法有两个参数,pred和node。

pred为当前节点的前趋节点。

node为当前节点。

该方法首先检查前趋结点的waitStatus位,如果为SIGNAL,表示前趋结点会通知它,那么它可以放心大胆地挂起了;

如果waitStatus>0也就是前趋结点是一个被取消的结点怎么办呢?那么就向前遍历跳过被取消的结点,直到找到一个没有被取消的结点为止,将找到的这个结点作为它的前趋结点,将找到的这个结点的waitStatus位设置为SIGNAL,返回false表示线程不应该被挂起。

如果不被挂起 , 那就会一直在acquireQueued方法的for循环里执行。


对于AQS,我自己也在读代码的阶段,如果后面发现有什么不对的地方,或者有理解更透彻的地方,我会更新本章节,也欢迎大家评论指正。

参考资料:

http://blog.csdn.net/aesop_wubo/article/details/7555956

http://ifeve.com/java-special-troops-aqs/

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

推荐阅读更多精彩内容