本部分主要参考《java并发编程艺术》一书相关内容,同时参考https://blog.csdn.net/zhilinboke/article/details/83104597,说的非常形象。
重入锁就是支持重入的锁,它表示该锁支持一个线程对资源的重复加锁。比如之前的在读AQS时的Mutex,在lock之后如果再次调用lock(),就会造成线程阻塞,这就不是重入锁。除此以外,ReentrantLock还可以支持公平锁跟非公平锁。对于这两种锁的支持,ReentrantLock实际上是内建了两个锁来分别实现的这两种锁,一个叫FairSync(公平锁),一个叫Sync(非公平锁)。
1、源码解读
对于非公平锁,获取同步状态的方法如下:
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); // 是当前线程占用,把同步状态数值累加(实际上多数是+1) return true; } return false; }
释放同步状态的代码,逻辑一样简单:
protected final boolean tryRelease(int releases) { int c = getState() - releases; // 每退出一层,就减去相应的数字 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { //同步状态变为0,说明全部释放了 free = true; setExclusiveOwnerThread(null); } setState(c); return free; }
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; }
锁的释放跟非公平锁一样。
通过以上逻辑,我们不难看出:公平锁跟非公平锁的区别就是获取同步资源的时候,公平锁要看一下是否有人排队,有则自己也排队;非公平锁的处理逻辑是不管有没人排队,先抢位置,抢到了就不管排队了,抢不到则跟公平锁一样进入排队;而对于排队队列里边的线程来说,公平锁跟非公平锁并没有什么区别,接下来都是按照排队顺序进行获取同步状态的。
2、测试与思考
对书中的测试程序作了轻微改动,让两个线程尽量无序的各自启动多个线程,然后观察执行顺序:
public class FairAndUnfairTest { private static Lock fairLock = new ReentrantLock2(true); private static Lock unfairLock = new ReentrantLock2(false); @Test public void fair() throws Exception { System.out.println("公平锁"); testLock(fairLock); } @Test public void unfair() throws Exception { System.out.println("非公平锁"); testLock(unfairLock); } public void testLock(Lock lock) throws Exception { new Thread(new Runnable() { @Override public void run() { for(int i=1; i<6; i++){ Thread thread = new Job(lock); thread.setName(""+i+""); thread.start(); mySleep(500); } } }).start(); Thread.sleep(500); new Thread(new Runnable() { @Override public void run() { for(int i=6; i<11; i++){ Thread thread = new Job(lock); thread.setName(""+i+""); thread.start(); mySleep(500); } } }).start(); Thread.sleep(11000); } private static class Job extends Thread{ private Lock lock; public Job(Lock lock){ this.lock = lock; } public void run(){ for(int i=0; i<2; i++){ lock.lock(); try { Thread.sleep(500); System.out.println("lock by [" + Thread.currentThread().getName() + "], waiting by " + ((ReentrantLock2)lock).getQueuedThread()); } catch (InterruptedException e) { e.printStackTrace(); } lock.unlock(); try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } } } } public static class ReentrantLock2 extends ReentrantLock{ public ReentrantLock2(boolean fair){ super(fair); } public Collection<String> getQueuedThread(){ List<Thread> arrayList = new ArrayList<Thread>(super.getQueuedThreads()); Collections.reverse(arrayList); List<String> list = new ArrayList<>(); for(Thread thread:arrayList){ list.add(thread.getName()); } return list; } } private void mySleep(int time){ try { Thread.sleep(time); } catch (InterruptedException e) { e.printStackTrace(); } } }
公平锁执行结果:

可以看到每一个都是执行队列中的head节点对应的线程,对于非公平锁执行结果之一:

可以看到排进队列的还是按照顺序执行的,但有偶尔被9之类的线程“插队”的情况。
去掉第58-61几行的扰乱代码,非公平锁结果如下:

可以看到非常整齐,每个线程连续执行了两次,书上的结论是:“(非公平锁)当一个线程请求锁时,只要获取了同步状态即成功获取锁。在这个前提下,刚释放锁的线程再次获取同步状态的几率会非常大,使得其他线程只能在同步队列中等待。”
这个,,,小的愚钝,读了几遍代码,没觉得能得出这个结论。个人觉得非公平锁无非就是多了一次判断有没有前置节点,这一点上比公平锁要费劲一点,其它的,感觉两者并无差别。至于一些地方描述的两者性能相差百倍的描述,,,,没太懂。书中所给的例子,我认为是因为程序的逻辑导致执行过程中恰好本线程释放后立即获取,因而一直是同一线程连续获得同步资源,避免了线程切换而导致看上去性能高很多而已,网上的多数文章得出两者性能相差百倍的例子,多数是时间差异导致,在每个线程消耗时间不确定的情况下,两者都需要切换线程,性能应该差别不会太过离谱,个人认为性能差距很大这个结论是不严谨的,有了解的请留言指正。
关于性能的讨论,顺便附个连接,这个讨论的也挺深入的:https://www.cnblogs.com/yulinfeng/p/6899316.html