其实没什么大不了,公平锁就是判断同步队列是否还有先驱节点的存在,只有没有先驱节点才能获取锁;而非公平锁是不管这个事的,能获取到同步状态就可以,就这么简单,那问题来了:
为什么会有公平锁/非公平锁的设计?考虑这个问题,我们需重新回忆上面的锁获取实现图了,其实上面我已经透露了一点
主要有两点原因:
原因一:恢复挂起的线程到真正锁的获取还是有时间差的,从人类的角度来看这个时间微乎其微,但是从CPU的角度来看,这个时间差存在的还是很明显的。所以非公平锁能更充分的利用 CPU 的时间片,尽量减少 CPU 空闲状态时间
原因二:不知你是否还记得我在 面试问,创建多少个线程合适? 文章中反复提到过,使用多线程很重要的考量点是线程切换的开销,想象一下,如果采用非公平锁,当一个线程请求锁获取同步状态,然后释放同步状态,因为不需要考虑是否还有前驱节点,所以刚释放锁的线程在此刻再次获取同步状态的几率就变得非常大,所以就减少了线程的开销
相信到这里,你也就明白了,为什么 ReentrantLock 默认构造器用的是非公平锁同步器
public ReentrantLock() { sync = new NonfairSync(); }看到这里,感觉非公平锁 perfect,非也,有得必有失
使用公平锁会有什么问题?
公平锁保证了排队的公平性,非公平锁霸气的忽视这个规则,所以就有可能导致排队的长时间在排队,也没有机会获取到锁,这就是传说中的 “饥饿”
如何选择公平锁/非公平锁?相信到这里,答案已经在你心中了,如果为了更高的吞吐量,很显然非公平锁是比较合适的,因为节省很多线程切换时间,吞吐量自然就上去了,否则那就用公平锁还大家一个公平
我们还差最后一个环节,真的要挺住
可重入锁到这里,我们还没分析 ReentrantLock 的名字,JDK 起名这么有讲究,肯定有其含义,直译过来【可重入锁】
为什么要支持锁的重入?
试想,如果是一个有 synchronized 修饰的递归调用方法,程序第二次进入被自己阻塞了岂不是很大的笑话,所以 synchronized 是支持锁的重入的
Lock 是新轮子,自然也要支持这个功能,其实现也很简单,请查看公平锁和非公平锁对比图,其中有一段代码:
// 判断当前线程是否和已占用锁的线程是同一个 else if (current == getExclusiveOwnerThread())仔细看代码, 你也许发现,我前面的一个说明是错误的,我要重新解释一下
重入的线程会一直将 state + 1, 释放锁会 state - 1直至等于0,上面这样写也是想帮助大家快速的区分
总结本文是一个长文,说明了为什么要造 Lock 新轮子,如何标准的使用 Lock,AQS 是什么,是如何实现锁的,结合 ReentrantLock 反推 AQS 中的一些应用以及其独有的一些特性
独占式获取锁就这样介绍完了,我们还差 AQS 共享式 xxxShared 没有分析,结合共享式,接下来我们来阅读一下 Semaphore,ReentrantReadWriteLock 和 CountLatch 等
最后,也欢迎大家的留言,如有错误之处还请指出。我的手酸了,眼睛干了,我去准备撸下一篇.....
灵魂追问
为什么更改 state 有 setState() , compareAndSetState() 两种方式,感觉后者更安全,但是锁的视线中有好多地方都使用了 setState(),安全吗?
下面代码是一个转账程序,是否存在死锁或者锁的其他问题呢?
class Account { private int balance; private final Lock lock = new ReentrantLock(); // 转账 void transfer(Account tar, int amt){ while (true) { if(this.lock.tryLock()) { try { if (tar.lock.tryLock()) { try { this.balance -= amt; tar.balance += amt; } finally { tar.lock.unlock(); } }//if } finally { this.lock.unlock(); } }//if }//while }//transfer } 参考Java 并发实战
Java 并发编程的艺术
https://tech.meituan.com/2019/12/05/aqs-theory-and-apply.html