本文属于java并发梳理系列。
锁作为并发共享数据,保证一致性的工具,在JAVA平台有多种实现(如 synchronized 和 ReentrantLock等等 ) 。本文梳理下ReentrantLock。作为依赖于AbstractQueuedSynchronizer。 所以要理解ReentrantLock,先要理解AQS。关系图如下所示:
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有过介绍,相关逻辑如下图所示;
AbstractQueuedSynchronizer中抽象了绝大多数Lock的功能,而只把tryAcquire方法延迟到子类中实现.其它的方法在AQS那边已经较为详细的梳理,本文为完整起见,只做简单说明。
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,也就是说这段代码实现了偏向锁的功能,并且实现的非常漂亮。
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来做。
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); } }
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