Skip to content

Commit e28846e

Browse files
committed
ReentrantLock加锁解读
1 parent 496e885 commit e28846e

2 files changed

Lines changed: 416 additions & 16 deletions

File tree

‎java/concurrency.html‎

Lines changed: 231 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
<html xmlns="http://www.w3.org/1999/xhtml" lang="en" xml:lang="en">
55
<head>
66
<title>资源争用模型(泛多线程编程)</title>
7-
<!-- 2017-09-27 三 22:48 -->
7+
<!-- 2017-09-28 四 22:38 -->
88
<meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
99
<meta name="generator" content="Org-mode" />
1010
<meta name="author" content="王月阳" />
@@ -761,18 +761,15 @@ <h4 id="sec-5-4-3"><span class="section-number-4">5.4.3</span> AQS解读</h4>
761761
前面我们所讲的,都是一种感性的认知。比如我们说所有的争用操作都会中某个资源状态互斥量上排队,比如我们说sync保证了线程
762762
安全,比如我们说ReadWriteLock实现了读写锁,这些都是感性的认知,我们会理解为这样就安全了。但这些到底是怎么实现呢?下
763763
面我们以AQS为例子,详细说明设计一个锁到底要包含哪些东西,需要哪些支撑,实现哪些功能。
764-
一个锁的设计要考虑的包含如下几个方面:
765764
</p>
766-
<ol class="org-ol">
767-
<li>抽象资源状态的标志位
768-
</li>
769-
<li>对标志位的原子复合操作
770-
</li>
771-
<li>对争用者行为的控制,包括暂停和恢复
772-
</li>
773-
<li>对争用者的暂存功能
774-
</li>
775-
</ol>
765+
766+
<p>
767+
<b>一个锁的设计要考虑的包含如下几个方面:</b>
768+
<b>1. 抽象资源状态的标志位</b>
769+
<b>2. 对标志位的原子复合操作</b>
770+
<b>3. 对争用者行为的控制,包括暂停和恢复</b>
771+
<b>4. 对争用者的暂存功能</b>
772+
</p>
776773

777774
<p>
778775
以上四个条件,就是设计一个锁的时候所要考虑的必要条件。将这四个全部实现,我们就可以设计出一个基本的锁来,然后再通过添
@@ -794,7 +791,227 @@ <h4 id="sec-5-4-3"><span class="section-number-4">5.4.3</span> AQS解读</h4>
794791
</p>
795792

796793
<p>
797-
下面我们通过分析JUC中ReentrantLock是怎么实现的,来看看怎么把这四个条件组合在一起就实现一个锁的。
794+
下面我们通过分析JUC中ReentrantLock是怎么实现的,来看看怎么把这四个条件组合在一起就实现一个锁的。要分析一个代码为啥这
795+
么写,要先分析这个代码提供了什么功能。那么ReentrantLock提供了什么功能呢,公平或者非公平的可重入锁。我们这里以公平的
796+
可重入锁来分析功能。公平的意思是线程按先来后到的顺序依次获取、使用和释放锁。可重入的意思是同一个线程在获得锁之后,再
797+
一次运行到获取锁的时候,不会被阻塞,释放的次数和获取的次数一样,才能释放掉锁。最后,它还实现了锁的功能。
798+
</p>
799+
800+
<p>
801+
ReentrantLock有个内部类叫做FairSync,它是AQS的一个子类,我们来梳理一下lock过程:
802+
</p>
803+
<ol class="org-ol">
804+
<li>尝试使用原子操作将抽象标志位(state)的值从0改变为1,这里0表示没有线程占用资源,1表示线程独占资源。
805+
1). 获取state值,判断是0的话表明锁未被占用,然后利用UnSafe提供的cas操作尝试将state的值设置为1如果设置成功则说明当
806+
前线程持有了锁。将锁的拥有者设为当前线程,返回成功标志
807+
2). 如果state的值不是0,说明锁已经被一个线程持有了,然后判断锁的持有者是不是当前线程,如果是的话将state的值加一,
808+
实现了线程重入次数的记录,后面释放锁的时候对于重入的线程,重入了几次就要做几次减一操作。返回成功标志
809+
</li>
810+
</ol>
811+
<div class="org-src-container">
812+
813+
<pre class="src src-java">/**
814+
* 获取公平锁的方法
815+
* 1)获取锁数量c
816+
* 1.1)如果c==0,如果当前线程是等待队列中的头节点,使用CAS将state(锁数量)从0设置为1,如果设置成功,当前线程独占锁--&gt;请求成功
817+
* 1.2)如果c!=0,判断当前的线程是不是就是当下独占锁的线程,如果是,就将当前的锁数量状态值+1(这也就是可重入锁的名称的来源)--&gt;请求成功
818+
* 最后,请求失败后,将当前线程链入队尾并挂起,之后等待被唤醒。
819+
*/
820+
protected final boolean tryAcquire(int acquires) {
821+
final Thread current = Thread.currentThread();
822+
int c = getState();
823+
if (c == 0) {
824+
if (isFirst(current) &amp;&amp; compareAndSetState(0, acquires)) {
825+
setExclusiveOwnerThread(current);
826+
return true;
827+
}
828+
}
829+
else if (current == getExclusiveOwnerThread()) {
830+
int nextc = c + acquires;
831+
if (nextc &lt; 0)
832+
throw new Error("Maximum lock count exceeded");
833+
setState(nextc);
834+
return true;
835+
}
836+
return false;
837+
}
838+
</pre>
839+
</div>
840+
<ol class="org-ol">
841+
<li>如果第一步尝试失败,会把当前线程放到CHL队列的尾部(公平锁与非公平锁的区别就在这里),放入尾部的这个操作实际上也是
842+
对CHL队列尾部资源的争用,这里是也是通过cas操作去实现的
843+
1). 创建一个新的CHL节点,存放当前线程;通过一次cas操作实现fast-try,就是快速尝试将节点放入到CHL队列尾部
844+
</li>
845+
</ol>
846+
<div class="org-src-container">
847+
848+
<pre class="src src-java">/**
849+
* 将Node节点加入等待队列
850+
* 1)快速入队,入队成功的话,返回node
851+
* 2)入队失败的话,使用正常入队
852+
* 注意:快速入队与正常入队相比,可以发现,正常入队仅仅比快速入队多而一个判断队列是否为空且为空之后的过程
853+
* @return 返回当前要插入的这个节点,注意不是前一个节点
854+
*/
855+
private Node addWaiter(Node mode) {
856+
Node node = new Node(Thread.currentThread(), mode);//创建节点
857+
/*
858+
* 快速入队
859+
*/
860+
Node pred = tail;//将尾节点赋给pred
861+
if (pred != null) {//尾节点不为空
862+
node.prev = pred;//将尾节点作为创造出来的节点的前一个节点,即将node链接到为节点后
863+
/**
864+
* 基于CAS将node设置为尾节点,如果设置失败,说明在当前线程获取尾节点到现在这段过程中已经有其他线程将尾节点给替换过了
865+
* 注意:假设有链表node1--&gt;node2--&gt;pred(当然是双链表,这里画成双链表才合适),
866+
* 通过CAS将pred替换成了node节点,即当下的链表为node1--&gt;node2--&gt;node,
867+
* 然后根据上边的"node.prev = pred"与下边的"pred.next = node"将pred插入到双链表中去,组成最终的链表如下:
868+
* node1--&gt;node2--&gt;pred--&gt;node
869+
* 这样的话,实际上我们发现没有指定node2.next=pred与pred.prev=node2,这是为什么呢?
870+
* 因为在之前这两句就早就执行好了,即node2.next和pred.prev这连个属性之前就设置好了
871+
*/
872+
if (compareAndSetTail(pred, node)) {
873+
pred.next = node;//将node放在尾节点上
874+
return node;
875+
}
876+
}
877+
enq(node);//正常入队
878+
return node;
879+
}
880+
</pre>
881+
</div>
882+
<p>
883+
2). 如果fast-try失败,就通过for(;;)循环尝试将节点加入到队列尾部去
884+
</p>
885+
<div class="org-src-container">
886+
887+
<pre class="src src-java">/**
888+
* 正常入队
889+
* @param node
890+
* @return 之前的尾节点
891+
*/
892+
private Node enq(final Node node) {
893+
for (;;) {//无限循环,一定要阻塞到入队成功为止
894+
Node t = tail;//获取尾节点
895+
if (t == null) { //如果尾节点为null,说明当前等待队列为空
896+
/*Node h = new Node(); // Dummy header
897+
h.next = node;
898+
node.prev = h;
899+
if (compareAndSetHead(h)) {//根据代码实际上是:compareAndSetHead(null,h)
900+
tail = node;
901+
return h;
902+
}*/
903+
/*
904+
* 注意:上边注释掉的这一段代码是jdk1.6.45中的,在后来的版本中,这一段改成了如下这段
905+
* 基于CAS将新节点(一个dummy节点)设置到头上head去,如果发现内存中的当前值不是null,则说明,在这个过程中,已经有其他线程设置过了。
906+
* 当成功的将这个dummy节点设置到head节点上去时,我们又将这个head节点设置给了tail节点,即head与tail都是当前这个dummy节点,
907+
* 之后有新节点入队的话,就插入到该dummy之后
908+
*/
909+
if (compareAndSetHead(new Node()))
910+
tail = head;
911+
} else {//这一块儿的逻辑与快速入队完全相同
912+
node.prev = t;
913+
if (compareAndSetTail(t, node)) {//尝试将node节点设为尾节点
914+
t.next = node;//将node节点设为尾节点
915+
return t;
916+
}
917+
}
918+
}
919+
}
920+
</pre>
921+
</div>
922+
<p>
923+
3). 入队成功后,有可能前面的节点的线程已经成功释放锁了,这时候刚刚入队的节点就会成为头节点,然后再执行步骤1去尝试
924+
获取锁,成功则返回
925+
</p>
926+
<div class="org-src-container">
927+
928+
<pre class="src src-java">final boolean acquireQueued(final Node node, int arg) {
929+
try {
930+
boolean interrupted = false;
931+
/*
932+
* 无限循环(一直阻塞),直到node的前驱节点p之前的所有节点都执行完毕,p成为了head且node请求成功了
933+
*/
934+
for (;;) {
935+
final Node p = node.predecessor();//获取插入节点的前一个节点p
936+
/*
937+
* 注意:
938+
* 1、这个是跳出循环的唯一条件,除非抛异常
939+
* 2、如果p == head &amp;&amp; tryAcquire(arg)第一次循环就成功了,interrupted为false,不需要中断自己
940+
* 如果p == head &amp;&amp; tryAcquire(arg)第一次以后的循环中如果执行了挂起操作后才成功了,interrupted为true,就要中断自己了
941+
*/
942+
if (p == head &amp;&amp; tryAcquire(arg)) {
943+
setHead(node);//当前节点设置为头节点
944+
p.next = null;
945+
return interrupted;//跳出循环
946+
}
947+
if (shouldParkAfterFailedAcquire(p, node) &amp;&amp; parkAndCheckInterrupt())
948+
interrupted = true;//被中断了
949+
}
950+
} catch (RuntimeException ex) {
951+
cancelAcquire(node);
952+
throw ex;
953+
}
954+
}
955+
</pre>
956+
</div>
957+
<p>
958+
4). 如果当前节点不是头节点,说明前面节点的线程还没有释放锁,那么就要通过判断当前节点的前一个节点的状态,来决定当
959+
前节点能不能被阻塞,如果可以利用LockSupport的功能将当前线程阻塞住;否则就把前面节点中,被取消的节点删除掉
960+
</p>
961+
<div class="org-src-container">
962+
963+
<pre class="src src-java">/**
964+
* 检测当前节点是否可以被安全的挂起(阻塞)
965+
* @param pred 当前节点的前驱节点
966+
* @param node 当前节点
967+
*/
968+
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
969+
int ws = pred.waitStatus;//获取前驱节点(即当前线程的前一个节点)的等待状态
970+
if (ws == Node.SIGNAL)//如果前驱节点的等待状态是SIGNAL,表示当前节点将来可以被唤醒,那么当前节点就可以安全的挂起了
971+
return true;
972+
/*
973+
* 1)当ws&gt;0(即CANCELLED==1),前驱节点的线程被取消了,我们会将该节点之前的连续几个被取消的前驱节点从队列中剔除,返回false(即不能挂起)
974+
* 2)如果ws&lt;=0&amp;&amp;!=SIGNAL,将当前节点的前驱节点的等待状态设为SIGNAL
975+
*/
976+
if (ws &gt; 0) {
977+
do {
978+
/*
979+
* node.prev = pred = pred.prev;
980+
* 上边这句代码相当于下边这两句
981+
* pred = pred.prev;
982+
* node.prev = pred;
983+
*/
984+
node.prev = pred = pred.prev;
985+
} while (pred.waitStatus &gt; 0);
986+
pred.next = node;
987+
} else {
988+
/*
989+
* 尝试将当前节点的前驱节点的等待状态设为SIGNAL
990+
* 1/这为什么用CAS,现在已经入队成功了,前驱节点就是pred,除了node外应该没有别的线程在操作这个节点了,那为什么还要用CAS?而不直接赋值呢?
991+
* (解释:因为pred可以自己将自己的状态改为cancel,也就是pred的状态可能同时会有两条线程(pred和node)去操作)
992+
* 2/既然前驱节点已经设为SIGNAL了,为什么最后还要返回false
993+
* (因为CAS可能会失败,这里不管失败与否,都返回false,下一次执行该方法的之后,pred的等待状态就是SIGNAL了)
994+
*/
995+
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
996+
}
997+
return false;
998+
}
999+
</pre>
1000+
</div>
1001+
<p>
1002+
利用LockSupport实现对线程的暂停操作
1003+
</p>
1004+
<div class="org-src-container">
1005+
1006+
<pre class="src src-java">private final boolean parkAndCheckInterrupt() {
1007+
LockSupport.park(this);//挂起当前的线程
1008+
return Thread.interrupted();//如果当前线程已经被中断了,返回true
1009+
}
1010+
</pre>
1011+
</div>
1012+
1013+
<p>
1014+
讲完加锁操作,我们再来看一下解锁是怎么实现的。
7981015
LockSupport+cas+state+CHL队列构成了Java锁的基础
7991016
</p>
8001017
</div>
@@ -849,7 +1066,7 @@ <h3 id="sec-7-2"><span class="section-number-3">7.2</span> Zookeeper一主多从
8491066
</div>
8501067
<div id="postamble" class="status">
8511068
<p class="author">Author: 王月阳</p>
852-
<p class="date">Created: 2017-09-27 三 22:48</p>
1069+
<p class="date">Created: 2017-09-28 四 22:38</p>
8531070
<p class="creator"><a href="http://www.gnu.org/software/emacs/">Emacs</a> 24.5.1 (<a href="http://orgmode.org">Org</a> mode 8.2.10)</p>
8541071
<p class="validation"><a href="http://validator.w3.org/check?uri=referer">Validate</a></p>
8551072
</div>

0 commit comments

Comments
 (0)