转载自并发编程网,连接地址: ReentrantLock(重入锁)以及公平性java
ReentrantLock的实现不只能够替代隐式的synchronized关键字,并且可以提供超过关键字自己的多种功能。编程
这里提到一个锁获取的公平性问题,若是在绝对时间上,先对锁进行获取的请求必定被先知足,那么这个锁是公平的,反之,是不公平的,也就是说等待时间最长的线程最有机会获取锁,也能够说锁的获取是有序的。ReentrantLock这个锁提供了一个构造函数,可以控制这个锁是不是公平的。 而锁的名字也是说明了这个锁具有了重复进入的可能,也就是说可以让当前线程屡次的进行对锁的获取操做,这样的最大次数限制是Integer.MAX_VALUE,约21亿次左右。并发
事实上公平的锁机制每每没有非公平的效率高,由于公平的获取锁没有考虑到操做系统对线程的调度因素,这样形成JVM对于等待中的线程调度次序和操做系统对线程的调度之间的不匹配。对于锁的快速且重复的获取过程当中,连续获取的几率是很是高的,而公平锁会压制这种状况,虽然公平性得以保障,可是响应比却降低了,可是并非任何场景都是以TPS做为惟一指标的,由于公平锁可以减小“饥饿”发生的几率,等待越久的请求越是可以获得优先知足。ide
在ReentrantLock中,对于公平和非公平的定义是经过对同步器AbstractQueuedSynchronizer的扩展加以实现的,也就是在tryAcquire的实现上作了语义的控制。函数
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; }
上述逻辑主要包括:测试
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; }
上述逻辑相比较非公平的获取,仅加入了当前线程(Node)以前是否有前置节点在等待的判断。hasQueuedPredecessors()方法命名有些歧义,其实应该是currentThreadHasQueuedPredecessors()更为妥帖一些,也就是说当前面没有人排在该节点(Node)前面时候队且可以设置成功状态,才可以获取锁。ui
protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { free = true; setExclusiveOwnerThread(null); } setState(c); return free; }
上述逻辑主要主要计算了释放状态后的值,若是为0则彻底释放,返回true,反之仅是设置状态,返回false。this
下面将主要的笔墨放在公平性和非公平性上,首先看一下两者测试的对比:操作系统
测试用例以下:线程
public class ReentrantLockTest { private static Lock fairLock = new ReentrantLock(true); private static Lock unfairLock = new ReentrantLock(); @Test public void fair() { System.out.println("fair version"); for (int i = 0; i < 5; i++) { Thread thread = new Thread(new Job(fairLock)); thread.setName("" + i); thread.start(); } try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } } @Test public void unfair() { System.out.println("unfair version"); for (int i = 0; i < 5; i++) { Thread thread = new Thread(new Job(unfairLock)); thread.setName("" + i); thread.start(); } try { Thread.sleep(5000); } catch (InterruptedException e) { e.printStackTrace(); } } private static class Job implements Runnable { private Lock lock; public Job(Lock lock) { this.lock = lock; } @Override public void run() { for (int i = 0; i < 5; i++) { lock.lock(); try { System.out.println("Lock by:" + Thread.currentThread().getName()); } finally { lock.unlock(); } } } } }
调用非公平的测试方法,返回结果(部分):
unfair version Lock by:0 Lock by:0 Lock by:2 Lock by:2 Lock by:2 Lock by:2 Lock by:2 Lock by:0 Lock by:0 Lock by:0 Lock by:1 Lock by:1 Lock by:1
调用公平的测试方法,返回结果:
fair version Lock by:0 Lock by:1 Lock by:0 Lock by:2 Lock by:3 Lock by:4 Lock by:1 Lock by:0 Lock by:2 Lock by:3 Lock by:4
仔细观察返回的结果(其中每一个数字表明一个线程),非公平的结果一个线程连续获取锁的状况很是多,而公平的结果连续获取的状况基本没有。那么在一个线程获取了锁的那一刻,究竟锁的公平性会致使锁有什么样的处理逻辑呢?
经过以前的同步器(AbstractQueuedSynchronizer)的介绍,在锁上是存在一个等待队列,sync队列,咱们经过复写ReentrantLock的获取当前锁的sync队列,输出在ReentrantLock被获取时刻,当前的sync队列的状态。
修改测试以下:
public class ReentrantLockTest { private static Lock fairLock = new ReentrantLock2(true); private static Lock unfairLock = new ReentrantLock2(); @Test public void fair() { System.out.println("fair version"); for (int i = 0; i < 5; i++) { Thread thread = new Thread(new Job(fairLock)) { public String toString() { return getName(); } }; thread.setName("" + i); thread.start(); } // sleep 5000ms } @Test public void unfair() { System.out.println("unfair version"); for (int i = 0; i < 5; i++) { Thread thread = new Thread(new Job(unfairLock)) { public String toString() { return getName(); } }; thread.setName("" + i); thread.start(); } // sleep 5000ms } private static class Job implements Runnable { private Lock lock; public Job(Lock lock) { this.lock = lock; } @Override public void run() { for (int i = 0; i < 5; i++) { lock.lock(); try { System.out.println("Lock by:" + Thread.currentThread().getName() + " and " + ((ReentrantLock2) lock).getQueuedNames() + " waits."); } finally { lock.unlock(); } } } } private static class ReentrantLock2 extends ReentrantLock { public ReentrantLock2() { super(); } public ReentrantLock2(boolean fair) { super(fair); } private static final long serialVersionUID = 1773716895097002072L; public List<String> getQueuedNames() { List<String> names = new ArrayList<String>(); Collection<Thread> threads = super.getQueuedThreads(); for(Thread thread : threads) { names.add(thread.getName()); } return names; } } }
上述逻辑主要是经过构造ReentrantLock2用来输出在sync队列中的线程内容,并且每一个线程的toString方法被重写,这样当一个线程获取到锁时,sync队列里的内容也就能够得知了,运行结果以下:
调用非公平方法,返回结果:
unfair version Lock by:0 and [] waits. Lock by:0 and [] waits. Lock by:3 and [2, 1] waits. Lock by:3 and [4, 2, 1] waits. Lock by:3 and [4, 2, 1] waits. Lock by:3 and [0, 4, 2, 1] waits. Lock by:3 and [0, 4, 2, 1] waits. Lock by:1 and [0, 4, 2] waits. Lock by:1 and [0, 4, 2] waits.
调用公平方法,返回结果:
fair version Lock by:0 and [] waits. Lock by:1 and [0, 4, 3, 2] waits. Lock by:2 and [1, 0, 4, 3] waits. Lock by:3 and [2, 1, 0, 4] waits. Lock by:4 and [3, 2, 1, 0] waits. Lock by:0 and [4, 3, 2, 1] waits. Lock by:1 and [0, 4, 3, 2] waits. Lock by:2 and [1, 0, 4, 3] waits.
能够明显看出,在非公平获取的过程当中,“插队”现象很是严重,后续获取锁的线程根本不顾及sync队列中等待的线程,而是能获取就获取。反观公平获取的过程,锁的获取就相似线性化的,每次都由sync队列中等待最长的线程(链表的第一个,sync队列是由尾部结点添加,当前输出的sync队列是逆序输出)获取锁。一个 hasQueuedPredecessors方法可以得到公平性的特性,这点其实是由AbstractQueuedSynchronizer来完成的,看一下acquire方法:
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
能够看到,若是获取状态和在sync队列中排队是短路的判断,也就是说若是tryAcquire成功,那么是不会进入sync队列的,能够经过下图来深入的认识公平性和AbstractQueuedSynchronizer的获取过程。 非公平的,或者说默认的获取方式以下图所示:
对于状态的获取,能够快速的经过tryAcquire的成功,也就是黄色的Fast路线,也能够因为tryAcquire的失败,构造节点,进入sync队列中排序后再次获取。所以能够理解为Fast就是一个快速通道,当例子中的线程释放锁以后,快速的经过Fast通道再次获取锁,就算当前sync队列中有排队等待的线程也会被忽略。这种模式,能够保证进入和退出锁的吞吐量,可是sync队列中过早排队的线程会一直处于阻塞状态,形成“饥饿”场景。
而公平性锁,就是在tryAcquire的调用中顾及当前sync队列中的等待节点(废弃了Fast通道),也就是任意请求都须要按照sync队列中既有的顺序进行,先到先得。这样很好的确保了公平性,可是能够从结果中看到,吞吐量就没有非公平的锁高了。