java并发系列:深入分析ReentrantLock

引子

锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized 和 ReentrantLock等等 ) 。 重入锁ReentrantLock,就是支持重进入的锁 ,它表示该锁能够支持一个线程对资源的重复加锁。支持公平性与非公平性选择,默认为非公平。本文梳理下ReentrantLock。作为依赖于AbstractQueuedSynchronizer。 所以要理解ReentrantLock,先要理解AQS。关系图如下所示:


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

    重进入是指任意线程在获取到锁之后能够再次获取该锁而不被锁所阻塞,需要解决两个问题:

1) 线程再次获取锁(锁需要识别获取锁的线程是否未当前占据锁的线程)

2)锁的最终释放(要求锁对于获取进行计数自增,锁释放技数自减。技数=0表示锁已经成功释放)

下面在分析实现的过程可以带着这个问题去思考。

分析:

我们日常使用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有两个子类:

static final class NonfairSync extends Sync {}
  static final class FairSync extends Sync {}
分别对应于非公平锁、公平锁,默认情况下为非公平锁。

公平锁:每个线程抢占锁的顺序为先后调用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有过介绍,相关逻辑如下图所示;


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) {//因为重入的关系,则进行多次释放,直至status==0则真正释放锁
                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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值