JDK5并发(2) Locks-ReentrantLock

Java.concurrent.locks(2)-ReentrantLock

@(Base)[JDK, locks, ReentrantLock, AbstractQueuedSynchronizer, AQS]

转载请写明:原文地址

系列文章:

-Java.concurrent.locks(1)-AQS
-Java.concurrent.locks(2)-ReentrantLock

ReentrantLock 顾名思义,可重入的独占锁。该对象与synchronized关键字有着相同的语义和表现,但是它还具有一些扩展的功能。可重入锁被最近的一个成功lock的线程占有(unlock后释放)。该类有一个重要特性体现在构造器上,构造器接受一个可选参数,是否是公平锁,默认是非公平锁。

公平锁:先来一定先排队,一定先获取锁。非公平锁:不保证上述条件。非公平锁的吞吐量更高(throughout),

可重入:同一个线程可以反复在同一个ReentrantLock对象上调用lock()方法,当然对应的是必须调用相同次unlock()方法。最大的递归次数是:2147483647。2的31次方-1

首先我们回顾一下上一篇文章Java.concurrent.locks(1)-AQS,底层实现了等待队列,我们只需要利用一个原子操作来更改内部状态表示是否锁住了,也就是最核心的tryAcquire()方法即可。

我们首先来看ReentrantLock的内部结构:

ReentrantLock
--> Sync
--> NonfairSync
--> FairSync
==> lock
==> lockInterruptibly
==> tryLock
==> tryLock
==> unlock
==> newCondition
==> hasQueuedThreads
==> hasQueuedThread
==> hasWaiters
==> getWaitQueueLength
==> toString
==> getWaitingThreads

首先-->表示内部类,==>表示方法,我们可清楚看到ReentrantLock有内部有3个同步器,Sync继承了AQS的基础同步器,一些公共方法都在这里,NonfairSync就是非公平的同步器,FairSync就是公平的同步器。

简单解释一下,当ReentrantLock构造器传入true,那么底层就使用FairSync,如果传入false,那么底层就使用NonfairSync

看到这里读者肯定会非常疑惑,到底怎么写的代码就是公平的,不是底层都用的AQS框架吗,底层不是就是一个链表在排队吗。为什么就不公平了呢。别着急,继续往下看。

Summary

其实看到这里,ReentrantLock我们可以看到两个重点,第一个是,如何实现可重入的。第二个是,怎么样的代码就是公平的,怎么样又是不公平的。所以下面我们就围绕这两个方面进行介绍。

Reentrant

可重入 前文已经解释了,同一个线程可以反复获取已经获取了的ReentrantLock
对象。根据我们对AQS框架的了解,在子类中我们只需要实现一个tryAcquire函数即可,这一点也是我们反复强调的。我们首先来看下,ReentrantLocktryAcquire是如何实现的。下面是一个非公平的版本(也就是默认版本),我们先不要在意公平两个字,我们的关键是“可重入”。

     final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

这段代码两个if就说明了问题。

  • 如果当前控制状态(c)是0,表示没有人占有,那么我们设置他为占有,并且把独占线程设为自己。
  • 如果当前控制状态不是0,说明有人正在占有,别急,看看这个人是不是自己。如果是自己,那么把控制状态继续累加。

看到这里似乎明白,如果有累加,那么释放的时候就是一个累减的过程,直到减到0,那么这个Lock才算释放,原来可重入的是这个意思!

Fair

公平,首先我们想了解的是,为什么会不公平。我们需要回到AQS的代码:

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt(); //
    }

我们来回顾一下这个步骤:

  • 调用子类抢占状态,如果成功,直接返回
  • 如果失败,那么当前线程入队之后,park
  • 处理中断异常

考虑如下情况:

  • 当前锁被线程A占用
  • 此时B挂起在队列中
  • C进入方法acquire

此时,恰好A释放,按理应该是队列中的B获取锁,这个符合先到先得的“公平策略”,但是B这是还挂起在,C就开始抢占资源,然后占有了之后直接退出acquire函数。

这里有个特殊的地方在于,A线程释放锁的最后一步,是唤醒队列第一个元素。参考release()方法。这时B唤醒之后,结果无锁可用,这时B又会继续park。所以在入队那边的操作是一个循环操作

从上面的现象来看,这就是我们不做任何措施的情况下,默认就是非公平锁。不要小看哦,这个确实能够提高这个锁的吞吐量哦,在一些关键场景下还是能发挥一定作用的!

我们下面仔细看一看非公平锁的实现:

      // 本方法在Lock.Sync中
      /**
      * Performs lock.  Try immediate barge, backing up to normal
      * acquire on failure.
      */
      final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
      }
      // 本方法在Lock.UnFairSync中
       /**
         * Performs non-fair tryLock.  tryAcquire is
         * implemented in subclasses, but both need nonfair
         * try for trylock method.
         */
        final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

我们需要注意的是这个实现真心非常巧妙。下面简单描述一下调用顺序:

  • 外部调用ReentrantLock.lock()
  • ReentrantLock调用内部基类Sync.lock(),也就是上文的方法1。
  • 内部的基类Sync.lock()如果判断没过,就调用AQS
  • AQS就调用UnfairSync.tryAcquire()

其他步骤都很好说,唯独在调用AQS之前多了一步if操作,也就是上面代码中的方法1。显然本类的作者非常清楚不公平的同步器就是为了提高吞吐量,并且也清楚占用一个锁,无非两步,第一设置控制状态,第二设置独占线程。

所以他就很豪迈地直接进来就先抢一次,抢到了,直接退出,没抢到再继续。给吞吐量一个更多的想像空间。

如何公平?

在AQS类的头部注释中写的非常明白:

Because checks in acquire are invoked before enqueuing, a newly acquiring thread may barge ahead of others that are blocked and queued. However, you can, if desired, define tryAcquire and/or tryAcquireShared to disable barging by internally invoking one or more of the inspection methods, thereby providing a fair FIFO acquisition order. In particular, most fair synchronizers can define tryAcquire to return false if hasQueuedPredecessors (a method specifically designed to be used by fair synchronizers) returns true. Other variations are possible.

我们只需要在tryAcquire()方法中调用该类定义的protected方法hasQueuedPredecessors()来优先判断队列中是否存在等待的人即可。

下面我们看ReentrantLock中公平锁的tryAcquire()方法的实现:

 /**
  * Fair version of tryAcquire.  Don't grant access unless
  * recursive call or no waiters or is first.
  */
 protected final boolean tryAcquire(int acquires) {
     final Thread current = Thread.currentThread();
     int c = getState();
     if (c == 0) {
         if (!hasQueuedPredecessors() &&
             compareAndSetState(0, acquires)) {
             setExclusiveOwnerThread(current);
             return true;
         }
     }
     else if (current == getExclusiveOwnerThread()) {
         int nextc = c + acquires;
         if (nextc < 0)
             throw new Error("Maximum lock count exceeded");
         setState(nextc);
         return true;
     }
     return false;
 }

果然如注释所写,公平的类只做了一件事情,就是在设置状态的时候检查一下队列中是否还有人在等待。

Summary

在本篇文章中着重介绍了ReentrantLock基于AQS框架的实现。着重介绍了两个特点,第一是可重入,第二是公平与否。在后续的文章中,我们将继续围绕ReentrantLock和他的小伙伴Condition进行一定的介绍。

posted @ 2016-02-23 21:49  马宇申  阅读(...)  评论(...编辑  收藏