JDK8的RentrantLock源码学习--非公平锁

jdk1.5开始增加了一种新的锁,原有的锁是通过对象头中的一个锁标识符来实现的,这里给大家分享一篇文章,http://www.jianshu.com/p/c5058b6fe8e5。

这里的实现我们且看且分析,先来看看类关系结构图,直接实现了Lock。

内部持有了一个Sync对象,根据描述可知,这个对象才是真正提供锁实现机制的核心。

这个对象继承自AbstractQueuedSynchronizer,子类支持公平和非公平两种模式,使用的是AQS状态去描述锁的持有数量。AQS是并发包下一个非常重要的类,之后会专门写一篇来讲解。

默认使用的是非公平锁,可以通过传入布尔值true来使用公平锁。

当我们调用lock命令的时候,其实真正调用的是sync对象的lock。

如果是非公平锁,我们来看看实现,首先调用一个CAS将一个状态从0变为1

这个方法是AQS里的一个方法,调用底层的UNSAFE的CASI更新状态,那么这个stateOffset是什么呢?

从AQS的静态初始化里可以看到这个是一个代表状态的内存偏移量,说直白点就是表示一种状态的值。

当更新成功后就进入了setExclusiveOwnerThread方法并传入了当前线程。然后这个方法也是AQS里面实现的,在AQS里面直接赋值给一个从名字分析来看叫唯一的线程的变量。

如果在上一步修改失败了进入acquire方法,这个方法里首先调用了一个treAcquire方法,并把传入的参数传进去,

TryAcquire方法在AQS里直接抛出了不支持操作的异常,这里调用的呢实际是nonFairSync里面重写的方法,这里又直接调用了nonfairTryAcquire方法,

在这个方法里首先调用了getState方法获得一个状态变量,直接调用了AQS里的方法,返回一个代表锁定次数的值。如果返回状态为0,则说明当前无线程锁定,则继续调用前面的CAS去修改状态为1,修改成功则设置当前线程为排他的主线程,然后返回true,或者返回false,也就是获取锁失败,如果状态不为0,则判断当前线程与排他的主线程是否相等,则改变加锁次数,如果这个次数小于0则抛出错误,然后调用setState方法把这个锁次数设置为状态,从这里可以理解为是一个可重入锁的实现过程

非公平下尝试获取锁
返回状态
更新状态

在前面如果调用tryAcquire失败后接着会调用acquireQueued方法,第一个参数里面先调用了一个addWaiter方法,并传入了一个代表排他的node节点,,这里面先对线程和这个代表着线程阻塞模式的状态包装为node,然后先拿到node节点的尾节点,如果这个尾节点不为null,则把这个为节点设置为新建立的node的前节点形成一个链条,然后通过CAS把尾节点替换为新建的node,然后把前面节点的下一个节点指向新建节点,然后返回。如果为节点为null或者替换失败则调用enq方法

enq方法里面调用一个线程循环,首先拿到尾节点,如果为尾节点为空,则先建立一个空的null节点并设置为Head,然后把尾节点也指向head,这相当于对node链的一个初始化,然后再次进入for循环。这时候头尾节点不为Null了,或者如果不为null,则把传入的新建node的前节点指向尾节点,然后通过CAS替换传入的node为尾节点,最后把之前的尾节点的next指向新传入的节点就完成了链条,这里其实仍然是一次重试node节点链条的建立,最后总是返回新建的节点。

写到这里我们看到这里利用node构建了一个链条,虽然目前不知道这个链条的作用,但是我们可以猜测到这个链条应该是用于阻塞等待的。在这个方法里面首先初始化了两个布尔值,一个用于判断获取结果是成功还是失败,一个用于判断线程状态。然后进入一个for循环。通过调用新传入的node.predecessor()方法获取这个Node的前一个node,如果前一个是整个链条的head,线程的阻塞等待是通过这个头节点的状态为-1实现的,然后就会再次尝试调用TraAcquire方法去获取,如果获取成功,则说明线程获得了锁,之后把获得锁的node(它持有当前线程)设置为头node,然后持有的线程设置为null,,然后把node的前一个node置为null,获取是否失败的标志变为false,然后返回是否中断的状态。如果前面的判断失败则调用一个shouldParkAfterFailedAcquire方法,从名字分析是一个获取失败后暂存线程的操作。真正的线程阻塞在parkAnCheckInterrupt里面调用locksupport.park方法实现的。

这个方法里第一个参数是新建Node的前一个Node,第二个参数是我们新建的node,他的主要目的是判断或者找到当前某一个node的等待状态为-1,这里如果node大于0,说明已经持有锁了,数值越大,说明重入次数越多,如果为0则说明在竞争锁。首先判断前一个node的线程状态是什么,如果是signal(-1)状态,则直接返回true,就可以安全的寄存线程了

如果不是则首先遍历node链条找到状态是0的节点,然后把我们新加进来的node变为这个节点的下一个节点,然后更新这个0的节点状态为-1.

在这个方法里如果返回true,则执行parkAndCheckInterrupt()方法:这里首先调用LockSupport的park方法把线程寄存,然后在判断线程的状态,使用interrupted和interrupt方法的区别还记得吗?如果调用了interrupted方法,会取消线程的中断状态。如果成功,则线程安全的寄存,如果寄存失败,则返回线程的中断状态并取消这个中断状态。

到这里就完成了线程的等待,这里的核心是调用了park方法实现的。

下面来看下unlock的实现。在这里调用了AQS的release  

在release方法里首先调用了一个tryRelease的方法,如果这个返回true,则先拿到头节点,然后判断头节点不为null且他的等待状态不为0,则调用unparkSuccessor方法。我们先看tryRelease方法

tryRelease(),首先对该线程的状态进行一个线程锁次数减操作,然后判断当前线程是不是被设置为排他的线程,如果不是就抛出异常,如果是则先初始化一个是否成功的布尔值,如果此时锁定次数为0,则直接设置这个布尔值为true,同时设置排他线程为null,最后调用setState改变这个线程的状态,最后返回状态,这里的状态有2种,一种是锁定次数变为0代表解锁成功,如果不为0则代表重入了多次,需要多次调用来释放。

返回true后我们看看unparkSuccessor()方法里面的实现,进来后首先拿到这个线程的等待状态,如果小于0,先变更为0

然后拿到这个节点的下一个节点,判断如果下一个节点不为Null且等待状态为-1或者0,直接调用LockSupport.unpark()方法,这里注意传入的对象是下一个节点,此时我们使用的是非公平锁,那么这个下一个节点s是由怎么决定的呢?之后我们会详细描述整个链条的构造过程。这里看如果等待状态>0呢,首先把s设置为null,然后从整个链条的尾部开始遍历,找到一个线程node的等待状态为0或者-1的然后把这个节点,然后释放这个节点。这里的意思是如果解锁失败的话会直接去尾部开始找一个等待的线程node去释放。

由这个方法里面我们可以推断出,解锁的顺序是按照获得锁的顺序进行操作的。测试代码如下:


t4代码和t3是一样的。

上面代码通过休眠100毫秒保证我们现场顺序入锁队列。经过多次运行打印顺序按照入队顺序。

入队顺序是通过addWaiter方法来实现整个链条的构建的。头是一个线程为null的节点,状态为-1,然后下一个指向第一个去抢锁的线程Node,依次用next指向下一个。

tryLock();方法顾名思义是尝试获取一个锁。这里调用了nonfairTryAcquire方法,前面已经介绍过了,如果获取失败则返回false。也就是获取失败。

tryLock还有一个带时间参数的,底层调用的是一个带参数的tryAcquireNanos方法,第一个是Long,第二个是代表时间单位。

这个方法里首先判断线程是否被中断,如果中断抛出异常,如果没中断则取调用tryAcquire去获取锁,如果获取成功直接返回,如果获取失败则调用doAcquireNanos方法,

doAcquireNanos方法里首先拿到排他的这个node,然后在for循环里去尝试获取锁

如果失败则调用parkNanos方法指定阻塞一定时间,然后继续进入循环,知道我们的等待时间为0,然后获取失败去调用cancelAcquire方法,

这个方法里首先判断这个node状态,如果已经为Null则直接返回。如果不为null,则先把node的线程设置为null,然后沿着线程的前指针寻找直到一个等待状态大于0的一个node,

然后把当前取消的节点和获取到锁的这个节点建立管理关系,然后将取消的节点状态设置为-1,然后看这时候是不是尾节点,如果是则把前面的那个节点设置为尾节点,然后把前面节点的下一个节点设置为Null。如果不是尾节点先判断是不是头节点,如果不是则判断这个Node状态是不是-1,如果不是则继续判断是不是0或者是-1 如果满足然后把这个节点状态先更新为-1,且这个节点的前置节点不为null,然后取到我们要取消的节点的下一个节点,如果下一个节点不为null则把前面节点和下一个节点直接建立关系。如果上面条件不满足则直接对要取消的节点执行解除寄存的操作,最后把要取消的节点的下一个节点指向自己,这样就不存在引用问题可以回收了。

到此为止我们等待阻塞获取也就看完啦。

lockInterruptibly方法之前在阻塞队列里见到了很多个这个尝试获取锁的方式,下面来看看他的实现,他也是调用了sync的acquireInterruptibly方法,

这里也会通过TryAcquire方法去尝试获取锁,如果获取成功直接返回,如果获取失败则调用doAcquireInterruptibly方法,

这个方法里首先把当前线程包装成一个排他的的Node节点,然后去尝试获取锁,这里与前面多线程竞争锁唯一不一致的地方就是当调用parkAndCheckInterrupt方法成功后直接抛出了异常。而前面的操作是如果调用这个成功后,则把interrupted是否中断的状态改为true返回。

这里我们需要理解,调用Thread.interrupted(),如果线程被中断则返回true,且释放中断,如果未被中断则返回false,  所以这种方式获取锁必须保证Thread未被中断才能获取到锁。如果Thread执行过中断,则获取锁失败。

还剩最后一个newCondition方法,这里就不列代码了,它总是调用对应的sync对象取获取一个condition。

至此我们新的锁的实现都可以很直观的明了了。这就是一个线程竞争入队的过程

这里补充一下,公平锁,就是很公平,在并发环境中,每个线程在获取锁时会先查看此锁维护的等待队列,如果为空,或者当前线程线程是等待队列的第一个,就占有锁,否则就会加入到等待队列中,以后会按照FIFO的规则从队列中取到自己

非公平锁比较粗鲁,上来就直接尝试占有锁,如果尝试失败,就再采用类似公平锁那种方式。

下面一篇会看公平锁。

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

推荐阅读更多精彩内容