java并发系列:深入分析ReentrantLock

本文属于java并发梳理系列。

引子

锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized 和 ReentrantLock等等 ) 。本文梳理下ReentrantLock。作为依赖于AbstractQueuedSynchronizer。 所以要理解ReentrantLock,先要理解AQS。关系图如下所示:

java并发系列:深入分析ReentrantLock_第1张图片

aqs有多神奇,让ReentrantLock没有使用更“高级”的机器指令,也不依靠JDK编译时的特殊处理,就完成了代码的并发访问控制。

分析:

我们日常使用Reentrantlocak,如下所示:

Lock lock = new ReentrantLock();
lock.lock();
//do sth
 lock.unlock();
ReentrantLock会保证 do something在同一时间只有一个线程在执行这段代码,或者说,同一时刻只有一个线程的lock方法会返回。其余线程会被挂起,直到获取锁。从这里可以看出,其实ReentrantLock实现的就是一个独占锁的功能:有且只有一个线程获取到锁,其余线程全部挂起,直到该拥有锁的线程释放锁,被挂起的线程被唤醒重新开始竞争锁。

我们看下实现过程,首先看lock方法:

 public void lock() {
        sync.lock();
    }

可见,是Lock接口的操作都委派到一个Sync类上,该类继承了AbstractQueuedSynchronizer:

    abstract static class Sync extends AbstractQueuedSynchronizer {
在之前介绍AQS的文章里说过:锁的API是面向使用者的,它定义了与锁交互的公共行为,而每个锁需要完成特定的操作也是透过这些行为来完成的,但是实现是依托给同步器来完成,这样贯穿就容易理解代码。Sync有两个子类:

<span style="font-size:18px;"><span style="color:#666666;"><span style="font-size: 14px; line-height: 35px;"> static final class NonfairSync extends Sync {}
  static final class FairSync extends Sync {}</span></span></span>
分别对应于非公平锁、公平锁,默认情况下为非公平锁。

公平锁:每个线程抢占锁的顺序为先后调用lock方法的顺序依次获取锁。

非公平锁:每个线程抢占锁的顺序不定,谁运气好,谁就获取到锁,和调用lock方法的先后顺序无关。

我们看下非公平锁的实现:

     final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
compareAndSetState(0, 1) 这个是尝试获取锁,把state的状态从0改为1表示取得锁.这个时候设置获取锁的线程就是当前线程. 

 protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
底层有unsafe方法实现。我们看下获取锁失败的情况,就是 acquire(1)。

acquire的事调用AQS来实现的。代码如下:

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
之前在AQS有过介绍,相关逻辑如下图所示;

java并发系列:深入分析ReentrantLock_第2张图片
AbstractQueuedSynchronizer中抽象了绝大多数Lock的功能,而只把tryAcquire方法延迟到子类中实现.其它的方法在AQS那边已经较为详细的梳理,本文为完整起见,只做简单说明。

tryAcquire

 protected final boolean tryAcquire(int acquires) {
            return nonfairTryAcquire(acquires);
        }
 final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();//获取当前线程
            int c = getState();//获取标志位
            if (c == 0) {//没有线程竞争锁
                if (compareAndSetState(0, acquires)) {//通过cas设置状态位
                    setExclusiveOwnerThread(current);//如果cas成功,则代表当前线程获取锁,把当前线程设置到aqs参数中
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {//因为ReentrantLock是可重入锁,这里判断获取锁的线程是不是当前线程
                int nextc = c + acquires;//每次重入+1,
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);//设置状态
                return true;
            }
            return false;
        }
到此,如果如果获取锁,tryAcquire返回true,反之,返回false。回到AQS的acquire方法继续执行 addWaiter

该方法会首先判断当前状态,如果c==0说明没有线程正在竞争该锁,如果不c !=0 说明有线程正拥有了该锁。

如果发现c==0,则通过CAS设置该状态值为acquires,acquires的初始调用值为1,每次线程重入该锁都会+1,每次unlock都会-1,但为0时释放锁。如果CAS设置成功,则可以预计其他任何线程调用CAS都不会再成功,也就认为当前线程得到了该锁,也作为Running线程,很显然这个Running线程并未进入等待队列。

如果c !=0 但发现自己已经拥有锁,只是简单地++acquires,并修改status值,但因为没有竞争,所以通过setStatus修改,而非CAS,也就是说这段代码实现了偏向锁的功能,并且实现的非常漂亮。

addWaiter

 private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

用当前线程去构造一个Node对象,然后加入到对尾。其中参数mode是独占锁还是共享锁,默认为null,独占锁。这里lock调用的是AQS独占的API。在队列不为空的时候,先尝试通过cas方式修改尾节点为最新的节点,如果修改失败,意味着有并发,这个时候才会进入enq中死循环,“自旋”方式修改。

将线程的节点接入到队里中后,当让还需要做一件事:将当前线程挂起!这个事,由acquireQueued来做。

acquireQueued

acquireQueued主要是已经追加到队列的线程节点(addWaiter方法返回值)进行阻塞,但阻塞前又通过tryAccquire重试是否能获得锁,如果重试成功能则无需阻塞,直接返回。
    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

补充一点:对线程的挂起及唤醒操作是通过使用UNSAFE类调用JNI方法实现的。unsafe.park(false, 0L);
到此为止,一个线程对于锁的一次竞争才告一段落,结果又两种,要么成功获取到锁(不用进入到AQS队列中),要么获取失败,被挂起,等待下次唤醒后继续循环尝试获取锁。具体可以参见aqs的队列那块。

解锁

现在我们看看解锁过程
   public void unlock() {
        sync.release(1);
    }
    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
unlock方法调用了AQS的release方法,同样传入了参数1,和获取锁的相应对应,获取一个锁,标示为+1,释放一个锁,标志位-1。

tryRelease有子类实现,我们看下实现方法:

protected final boolean tryRelease(int releases) {
            int c = getState() - releases;//释放,-1
            if (Thread.currentThread() != getExclusiveOwnerThread())//判断释放的线程跟获取锁的线程是否一致
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {//因为重入的关系,<span style="font-family: Arial; font-size: 14px; line-height: 26px;">则进行多次释放,直至status==0则真正释放锁</span>
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

释放锁,成功后,找到AQS的头节点(Head),并唤醒它即可unparkSuccessor:

private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
        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);
    }
总结:

本文我们从ReentrantLock出发,分析了AQS独占功能的非公平锁的实现,尤其对于之前介绍AQS文章,增加了子类实现的tryAcquire、tryRelease等关键方法。基本思路AQS还是使用的阻塞的CHL队列的方式,而对该队列的操作均通过Lock-Free(CAS)操作,记录获取锁、竞争锁、释放锁等 一系列锁的状态。
synchronized的底层也是一个基于CAS操作的等待队列,但JVM实现的更精细,把等待队列分为ContentionList和EntryList,目的是为了降低线程的出列速度;当然也实现了偏向锁,从数据结构来说二者设计没有本质区别。但synchronized还实现了自旋锁,并针对不同的系统和硬件体系进行了优化,而Lock则完全依靠系统阻塞挂起等待线程。
当然Lock比synchronized更适合在应用层扩展,更灵活如可实现公平锁、非公平锁、读写锁。在业务并发简单清晰的情况下推荐synchronized,在业务逻辑并发复杂,或对使用锁的扩展性要求较高时,推荐使用ReentrantLock这类锁。

插一句:建议先理解AQS,再来看ReentrantLock。

参考:http://ifeve.com/jdk1-8-abstractqueuedsynchronizer/

http://blog.csdn.net/chen77716/article/details/6641477

你可能感兴趣的:(并发,线程安全,锁,AQS)