Java同步器框架剖析

Java同步器框架剖析

         同步器(Synchronizer)框架是java并发的核心基础,充分理解其设计原理能够更精准的使用/扩展部分工具,进而提高应用程序的性能。本文的分析依托源码,纯属个人见解。希望阅读者能够批判阅读。

         闲话少说,我们直接来说AbstractQueuedSynchronizer类,这个是同步器框架的核心类。它的实现思路:1,用volatile+原子操作来维护同步状态,2,如果锁获取失败将需要排队,3,通过LockSupport进行线程阻塞/唤醒操作。

我们先来看一下同步器的结构图,然后针对每个步骤进行逐一分析。

 

如图,我们很容易将此图转化成uml类图,我们也能够看出这是模板方法设计模式,AbstractQueuedSynchronizer把锁的获取/释放交给子类去处理。它本身所处理的问题是一旦获取失败/取消成功以后需要怎么做。

         我们从ReetrantLock锁开始根据调用过程来分析一次锁获取过程:当我们执行了lock.lock();意味着我们直接调用了AbstractQueuedSynchronizeracquire方法。Acquire的代码如下:

1tryAcquire方法,这个是子类方法,上文提到了,子类负责锁的获取,框架本身负责失败后的处理。如果1成功,直接返回了,如果1失败,转2

2addWaiter方法,这个方法的核心目标是将操作失败的线程添加到队列的尾部。我通过图来简单解释一下这个队列,因为他跟我们常规所认识的队列概念有些出入。根据注释可知这个是CLH queue队列的改进版,不过这些概念已经不重要了,主要是我们通过自己的分析能完全理解它的设计思路。

 

我们把这个大框看做是AbstractQueuedSynchronizer类,Headtail是其中两个属性,同时他们分别又指向了队列的头部跟尾部,队列在插入的时候从尾部添加,删除的时候从头部移除。

假设当前的内存如上图所示,这时候调用了addWaiter方法操作过程如下

A直接将新增节点添加到链表尾部,

B将尾部节点的引用指向新增节点

通过以上两个步骤完成了线程的排队,代码如下:

 这里需要解释两点:1,对于尾部更新compareAndSetTail(pred, node)是采用了原子操作。因为是原子操作,所以无论此时有多少线程并发执行该方法,只有一个能够执行成功,其他线程将进入enq方法循环尝试,直到更新成功为止。这里的实现思路是典型的乐观锁形式,相比于线程切换,让cpu多尝试几次往往会大大提供程序的伸缩性,如果你的应用因为太多的阻塞/线程频繁调度而烦恼,不妨试试这种方法。2,这种非常规的数据结构有什么好处么?首先是将插入和删除分开,从而减少了访问热点。其次相对来说比较容易找到需要唤醒的节点。

3acquireQueued 方法,这个方法不太好理解。,简单来说他主要做了两件事儿。A阻塞当前线程。B一旦有线程唤醒该线程,尝试重新获得锁。

以下是这段代码的核心部分:

  for (;;) {

                                    

                final Node p = node.predecessor();

               //如果前置队列是头结点并且获取锁成功,

if (p == head && tryAcquire(arg)) {//a

                    setHead(node);

                    p.next = null; // help GC

                    failed = false;

                    return interrupted;

                }

                if (shouldParkAfterFailedAcquire(p, node) &&

                    parkAndCheckInterrupt())//b

                    interrupted = true;

            }

假设当前线程执行的情况是2中添加第四个节点的情况,那么程序将直接执行b操作部分。
b
操作的核心就是阻塞当前线程,更具体的是通过LockSupport类的park方法来实现的。如果你想了解LockSupport到底是怎么实现阻塞的,那么抱歉了,jdk中并没有提供其实现源码。本质上,他也不是jdk能够实现得了的。Java的线程原生的映射到了操作系统的轻量级进程,能够直接阻塞操作系统进程的,恐怕只有操作系统提供的系统调用了。我们可以简单的把它理解为 java调用操作的系统的一个包装。至此,一个获取锁的操作在获取失败后完全阻塞了。

         程序走到此似乎已经到了尽头了,也的确如此,如果没有人来叫醒他,他将长睡不起。但是分析还得继续才行,假设此时有线程执行完调用了release方法,release的实现思路非常简单,如果当前线程释放成功,就用LockSupport唤醒最前头的线程节点。假设Nd4节点之前的节点都已经执行完了,那么回到当前线程,他从b操作醒来,继续执行,从而回到a操作,有一个小注意点是头节点是空节点,从第二个起才会存储真正阻塞的线程,所以有了node.predecessor()操作。注意:此处当前线程要重新调用tryAcquire重新竞争,运气不好他会失败继续阻塞的,这也是非公平锁的实现核心。

         至此,同步框架的分析接近尾声了,缪缪几行代码,但是完全理解起来并没有吃点甜点来的轻松。回顾一下1,他通过模板方法设计模式构造整个架构,我们可以通过继承来灵活的扩展适合我们的同步工具。2,他通过volatile+原子操作的int变量来维护锁状态。可以说原子操作和volatile的结合构成了整个并发框架的基础。Volatile是很早就出现在我们视野中的修饰词,但是一直抑郁不得志,因为单纯的可见性并无法解决并发问题。单纯的原子操作也无非只是一个计算机指令,他可以保证同时只有一个操作成功,但是没法保证更新后对其他线程立刻可见,java的线程模型有时候也是很蛋疼的东西。只有把这两个东西结合在一起,这让我想起了‘金风玉露一相逢’的诗句,算了,这个扯的有点多了。3,他用了一个变形的双向链表结构进行线程排队,优点已经说过了,好的数据结构我们可以借鉴。4,他是通过LockSupport系统工具来完成线程阻塞,唤醒的。5,他设计的精巧之处就是抢占式思维,这是他能够提供性能的主要手段之一。

 

猜你喜欢

转载自lncyhhq.iteye.com/blog/2232170