AQS同步队列器之二:实现原理(锁获取、锁释放)

一、类结构

  

   从图中可以看出来,AbstractQueuedSynchronizer继承自AbstractOwnableSynchronizer,那么AbstrcatOwnableSynchronizer是干嘛的呢?这不是这篇文章研究的重点,当然这个类也很简单就是提供了一些简单的操作比如设置独占锁拥有线程等方法。而这篇文章更关注于AQS的实现。

二、实现原理

   Node是AbstractQueuedSynchronizer的一个内部类,有时候我会自己去考虑当众多的线程去访问一个锁而获取锁的线程却只有一个(针对独占锁而言)、当然对于共享式锁也是可以有多个线程同时获取锁的。那么肯定会有些线程无法获取锁,那么它们存放在哪呢。Node就是它们的归宿也就是第一个关键点同步队列。那么可以看一下Node类里实现了什么。

值得研究的地方是构造函数以及一个很关键的waitStatus的状态值。同步队列的头节点也就是当前持有锁的线程对象。waitStatus【volatile修饰、当前线程节点的状态,volatile修饰是为了内存可见性】

    SIGNAL 值为-1、后继节点的线程处于等待的状态、当前节点的线程如果释放了同步状态或者被取消、会通知后继节点、后继节点会获取锁并执行【当一个节点的状态为SIGNAL时就意味着在等待获取同步状态,前节点是头节点也就是获取同步状态的节点】

    CANCELLED 值为1、因为超时或者中断,结点会被设置为取消状态,被取消状态的结点不应该去竞争锁,只能保持取消状态不变,不能转换为其他状态。处于这种状态的结点会被踢出队列,被GC回收【一旦一个节点进入这个状态也就会从同步队列中消失】

    CONDITION 值为-2、节点在等待队列中、节点线程等待在Condition、当其它线程对Condition调用了singal()方法该节点会从等待队列中移到同步队列中【从等待队列到同步队列】

    PROPAGATE 值为-3、表示下一次共享式同步状态获取将会被无条件的被传播下去【与共享模式相关代表线程可以执行】

    initial 值为0、表示当前没有线程获取锁【初始状态】

了解了节点等待的状态以及同步队列的作用,AQS中还通过了一个volatile关键字修饰的status对象用来管理锁的状态并提供了getState()、setState()、compareAndSetStatus()三个方法改变status的状态。知道了这些就可以开始真正看AQS是如何处理没有获取锁的线程的。

三、源码实现

独占式 

    acquire(int arg):独占式的获取锁

1         public final void acquire(int arg){
3                 if(!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE))){
5                         selfInterrupt();
7                 }
9         }

    acquire方法代码很少,但是它做了很多事,首先前面介绍过tryAcquire()方法是子类实现的具体获取锁的方法,当锁获取到了就会立刻退出if条件也就代表获取锁具体的就是啥也不干。那么看锁获取失败具体干了啥呢。首先是addWaiter(Node.EXCLUSIVE)方法

    addWaiter(Node mode):往同步队列中添加元素

 1         private Node addWaiter(Node mode){
 2           //通过当前线程和锁模式创建了一个Node节点
 3                 Node node = new Node(Thread.currentThread(),mode);
 4           //获取尾节点
 5                 Node pred = tail;
 7                 if(pred != null){
 9                     node.prev = pred;//新增的节点每次都是加在同步队列的尾部
10                     //通过CAS操作设置尾节点防止线程不安全
11                     if(compareAndSetTail(pred,node)){
13                             pred.next = node;
15                             return node;
17                     }
19                 }
21                 enq(node);//防止CAS操作失败,再次处理
23                 return node;
25             }

     addWaiter方法主要做的就是创建一个节点,如果通过CAS操作成功就直接将节点加入同步队列的尾部,否则需要enq方法的帮忙再次进行处理

    

    enq(Node node):在addWaiter方法处理失败的时候进一步进行处理

 1         private Node enq(final Node node){
 2           //死循环【发现很多的底层死循环都是这么写不知道是不是有什么优化点】
 3                 for(;;){
 5                      Node t = tail;
 7                      if(t == null){//如果尾节点为null
 9                             if(compareAndSetHead(new Node())){//创建一个新的节点并添加到队列中初始化
11                                     tail = head;
13                             }else{
15                                     node.prev = t;
                        //还是通过CAS操作添加到尾部 17 if(compareAndSetTail(t,node)){ 19 t.next = node; 21 return t; 22 } 23 } 25 } 27 } 28 }

     enq方法就是通过死循环,不断的通过CAS操作设置尾节点,直到添加成功才返回。比较狠,不到黄河不死心哈哈。

    acquireQueued(final Node node,int arg):当线程获取锁失败并加入同步队列以后,就进入了一个自旋的状态,如果获取到了这个状态就退出阻塞状态否则就一直阻塞

 1         final boolean acquireQueued(final Node node,int arg){
 3                 boolean failed = true;//用来判断是否获取了同步状态
 5                 try{
 7                         boolean interrupted = false;//判断自旋过程中是否被中断过
 9                         for(;;){
11                                 final Node p = node.predecessor();//获取前继节点
13                                 if(p == head && tryAcquire(arg)){//如果当前的这个节点的前继节点是头节点就去尝试获取了同步状态
15                                         setHead(node);//设为头节点
17                                         p.next = null;
19                                         failed = false;//代表获取了同步状态
21                                         return interrupted;
22                           }
23                  //判断自己是否已经阻塞了检查这个过程中是否被中断过
24                           if(shouldParkAfterFailedAcquire(p,node) && parkAndCheckInterrupt() ){
26                                 interrupted = true;
28                            }
30                     }finally{
32                             if(failed){
34                                     cancelAcquired(node);//取消加入同步队列中
36                             }
37                     }
39                 }
41             }

     acquireQueued方法主要是让线程通过自旋的方式去获取同步状态,当然也不是每个节点都有获取的资格,因为是FIFO先进显出队列,acquireQueued方法保证了只有头节点的后继节点才有资格去获取同步状态,如果线程可以休息了就让该线程休息然后记录下这个过程中是否被中断过,当线程获取了同步状态就会从这个同步队列中移除这个节点。

    shouldParkAfterFailedAcquire(Node node,Node node):判断一个线程是否阻塞

 1         private static boolean shouldPArkAfterFailedAcquire(Node pred,Node node){
 3                 int ws = pred.waitStatus;//获取节点的等待状态
 5                 if(ws == Node.SIGNAL){//如果是SIGNAL就代表当头节点释放后,这个节点就湖区尝试获取状态
 7                         return true;//代表阻塞中
 9                 }
11                 if(ws > 0){//代表前继节点放弃了
13                     do {
15                             node.prev = pred = pred.prev;//循环不停的往前找知道找到节点的状态是正常的
17                     }while(pred.waitStatus > 0 );
19                     pred.next = node;
21                 }else{
23                     compareAndSetWaitStatus(pred,ws,Node.SIGNAL);//通过CAS操作设置状态为SIGNAL
25                 }
27                 return false;
29             }

     整个流程中,如果前驱结点的状态不是SIGNAL,那么自己就不能安心去休息,需要去找个安心的休息点,同时可以再尝试下看有没有机会轮到自己拿号

    parkAndCheckInterrupt():前面的方法是判断是否阻塞,而这个方法就是真正的执行阻塞的方法同时返回中断状态

1         private final boolean parkAndCheckInterupt(){
2 
3                     LockSupport.park(this);//阻塞当前线程
4 
5                     return Thread.interrupted();//返回中断状态
6 
7         }

经过了上面的这么多方法,再次回头看acquire方法的时候。会发现其实整个流程也没有想象中的那么难以理解。acquire流程

    首先通过子类判断是否获取了锁,如果获取了就什么也不干。

    如果没有获取锁、通过线程创建节点加入同步队列的队尾。

    当线程在同步队列中不断的通过自旋去获取同步状态,如果获取了锁,就从同步队列中移除。

    如果在获取同步状态的过程中被中断过最后自行调用interrupted方法进行中断操作。

这里可以看一下acquire也就是独占式获取锁的整个流程

 上面是独占式的获取锁的整个流程,其中比较重要的就是队列的头节点是当前获取同步状态的节点,而出于FIFO的原则当当前的线程释放同步状态时其后继节点将为下一个能获取同步状态的节点。

    release(int arg):独占式的释放锁

 1         public final boolean release(int arg){
 2 
 3                 if(tryRelease(arg)){//子类自定义实现
 5                         Node h = head;
 7                         if(h != null && h.waitStatus != 0){
 9                                 unparkSuccessor(h);//唤醒下一个节点
11                          }
13                         return true;
14                 }
16                 return false;
18             }

    释放锁的流程很简单,首先子类自定义的方法如果释放了同步状态,如果头节点不为空并且头节点的等待状态不为0就唤醒其后继节点。主要依赖的就是子类自定义实现的释放操作。

    unparkSuccessor(Node node):唤醒后继节点获取同步状态

        private void unparkSuccessor(Node node){
           //获取头节点的状态
                 int ws = node.waitStatus;
                 if(ws < 0){
                        compareAndSetWaitStatus(node,ws,0);//通过CAS将头节点的状态设置为初始状态
                 }
                 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);//真正的唤醒操作
                 }
            }

     唤醒操作,通过判断后继节点是否存在,如果不存在就寻找等待时间最长的适合的节点将其唤醒唤醒操作通过LockSupport中的unpark方法唤醒底层也就是unsafe类的操作。

以上就是独占式的获取锁以及释放锁的过程总结的来说:线程获取锁,如果获取了锁就啥也不干,如果没获取就创造一个节点通过compareAndSetTail(CAS操作)操作的方式将创建的节点加入同步队列的尾部,在同步队列中的节点通过自旋的操作不断去获取同步状态【当然由于FIFO先进先出的特性】等待时间越长就越先被唤醒。当头节点释放同步状态的时候,首先查看是否存在后继节点,如果存在就唤醒自己的后继节点,如果不存在就获取等待时间最长的符合条件的线程。

共享式 

在了解如何共享式的获取锁之前,先介绍一下独占式和共享式:

    独占式:当一个线程获取锁以后,任何其它的线程无法再次获取到这个锁,也就是说在同一时刻只有一个线程能获取到同步状态,其它的线程无法获取锁状态只能阻塞等待

    共享式:当一个线程获取到共享同步状态时,其它线程也能获取到共享的同步状态,但是独占状态就会被阻塞

  

    acquireShared(int arg):共享式的获取锁 

1         public final void acquireShared(int arg){
2           //子类自定义实现的获取状态【也就是当返回为>=0的时候就代表获取锁】
3                 if(tryAcquireShared(arg) < 0){
5                         doAcquiredShared(arg);//具体的处理没有获取锁的线程的方法
7                 }
9         }

    当自定义返回的获取状态小于0就代表获取共享锁失败

    doAcquiredShared(int arg):处理未获取同步状态的线程

 1         private void doAcquire(int arg){
 3                 final Node node = addWaiter(Node.SHARED);//创建一个节点加入同步队列尾部
 5                 boolean failed = true;//判断获取状态
 7                 try{
 9                     boolean interrupted = false;//是否被中断过
11                     for(;;){
13                         final Node p =node.predecessor();//获取前驱节点
15                         if(p == head){
17                                 int r = tryAcquireShared(arg);//获取同步状态
19                                 if(r >= 0 ){//大于0代表获取到了
21                                        setHeadAndPropagate(node,r);//设置为头节点并且如果有多余资源一并唤醒
23                                        p.next = null;
25                                        if(interrupted){
27                                             selfInterrupted();//自我中断
29                                      }
31                             failed = false;
32                             return;
33                         }
34                     }
              //判断线程是否可以进行休息如果可以休息就调用park方法
35 if(shouldParkAfterFailedAcquire(p,node) && parkAndCheckInterrupt()){ 36 interrupted = true; 37 } 38 }finally{ 40 if(failed){ 42 cancelAcquire(node);//从同步队列中删除 44 } 45        } 46         } 47       }

     共享式获取锁和独占式唯一的区别在于setHeadAndPropagate这个方法,独占式的锁会去判断是否为后继节点,只有后继节点才有资格在头节点释放了同步状态以后获取到同步状态而共享式的实现依靠着setHeadAndPropagate这个方法

     setHeadAndPorpagate(Node node,int arg):获取共享同步状态以后的操作

 1         private void setHeadAndPropaGate(Node node,int propagate){
 3                Node h = head;
 4                setHead(node);//设置为头节点
 5                if(propagate >0 || h == null || h.waitStatus < 0){//大于0代表还有其他资源一并可以唤醒
 7                         Node s = node.next;//下一个节点
 8                         if(s == null || s.isShared()){
 9                                 doReleaseShared();
10                         }
11                 }
12         }

     这个方法主要的目的就是将获取到同步状态的节点设置为头节点、如果存在多个资源就将多个资源一并唤醒

     doReleaseShared():唤醒后继节点

 1         private void doReleaseShared(int arg){
 3                 for(;;){
 5                     Node h = head;
 7                     if(h != null && h != tail){
 9                         int ws = h.waitStatus;//获取头节点的等待状态
11                         if(!compareAndSetWaitStatus(h,Node.SIGNAL,0)){//设置不成功就一直进行设置
13                                 continue;
15                          }
17                          unparkSuccessor(h);//唤醒后继节点
19                     }else if (ws == 0 &&!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
21                           continue;                
22                 }
23                 if (h == head)                   
24                     break;
25         }            

 OK,至此,共享式的获取锁也研究过了。让我们再梳理一下它的流程

    1. tryAcquireShared()尝试获取资源,成功则直接返回;
    2. 失败则通过doAcquireShared()进入等待队列park(),直到被unpark()/interrupt()并成功获取到资源才返回。整个等待过程也是忽略中断的。

其实跟acquire()的流程大同小异,只不过多了个自己拿到资源后,还会去唤醒后继队友的操作(这才是共享嘛)

    releaseShared():释放共享同步状态

 1         public final boolean releaseShared(int arg){
 2             //子类自定义释放锁操作true代表释放
 3                    if(tryReleaseShared(arg)){
 5                         doReleaseShared();//处理释放的操作
 7                         return true;
 9                     }
11            }

     通过子类自定义实现的释放锁操作判断,如果未释放就什么也不干,而doReleased方法就是去唤醒当前的后继节点

四、总结

   本节我们详解了独占和共享两种模式下获取-释放资源(acquire-release、acquireShared-releaseShared)的源码,相信大家都有一定认识了。值得注意的是,acquire()和acquireSahred()两种方法下,线程在等待队列中都是忽略中断的。AQS也支持响应中断的,acquireInterruptibly()/acquireSharedInterruptibly()即是,这里相应的源码跟acquire()和acquireSahred()差不多,这里就不再详解了。

================================================================================== 

不管岁月里经历多少辛酸和艰难,告诉自己风雨本身就是一种内涵,努力的面对,不过就是一场命运的漂流,既然在路上,那么目的地必然也就是前方。


==================================================================================

猜你喜欢

转载自www.cnblogs.com/wait-pigblog/p/9321029.html