多线程-公平锁与非公平锁
2.什么是公平锁与非公平锁?
公平锁和非公平锁其实说的是获取锁的机会是不是对每个等待线程都公平。
这就好比一群学生都在竞争一个答题名额:
每次都是同一个学生抢到,这对其他学生不公平。
公平锁就是获取锁的机会对每个等待线程都公平。
非公平锁就是获取锁的机会对每个等待线程都不公平。
3.隐式锁非公平锁例子
我们先来看看非公平锁的例子,然后下一小节将非公平锁的例子改写为公平锁。
非公平锁例子很简单,这里我们先写一个匿名内部类实现Runnable接口的对象:
先不着急把run()方法写好,先来把线程创建好。
需要锁的地方一定是多个线程,所以这里创建3个线程,并把runnable对象传递给线程:
紧接着,我们启动线程的代码也写了:
接下来,我们来完善run()方法内部。在run()方法内部需要做的是,让这些线程不停地去竞争这把锁,在竞争的过程中一定有其他两个线程在等待这把锁,因为在任何时刻有且仅有一个线程拥有这把锁,所以,不停地做某事就用while循环:
其次是我们要让线程们去竞争锁,那就必须要有同步:
接下来,我们在同步代码块里面输出一句话,格式就是当前线程名称+其他语句:
最后,我们让线程执行到同步代码块里面的时候先睡1秒钟,因为担心执行太快,打印输出语句太多不方便观察:
好了,例子写完了,运行程序,执行结果:
从运行结果来看,非公平锁的效果产生了,从头到尾都是“Thread-0”在运行,其他两个线程从未拿到过锁。获取锁的机会是很不公平的。
该例子中还有一个特别重要的知识点:隐式锁是非公平锁。
隐式锁,即synchronized关键字用的锁。在上一章《“全栈2019”Java多线程第二十七章:Lock获取lock/释放unlock锁》中有介绍过,感兴趣的小伙伴可以前去阅读。
4.显式锁非公平锁例子
显式锁即Lock。在上一章《“全栈2019”Java多线程第二十七章:Lock获取lock/释放unlock锁》中也有介绍过,感兴趣的小伙伴可以前去阅读。
下面我们就来用显式锁改写上一小节例子。
首先,我们创建出显式锁对象:
其次,我们将同步代码块开始的地方换成lock.lock(),同步代码块结束的地方换成lock.unlock()。即这两个地方:
换完之后的样子:
好了,例子用显式锁改写完成。
接下来运行程序,观察执行结果:
从运行结果来看,符合预期。
为什么说符合预期?
因为显式锁默认就是非公平锁,所以说符合我们的预期。
难道显式锁还有公平锁?
是的,这就是我们使用显式锁Lock的好处。
5.显式锁公平锁例子
将显式锁非公平锁变成公平锁只需一步,在创建ReentrantLock对象时,给构造方法参数里传入true即可。
下面,我们来试试:
例子改写完成,是不是很简单。
接下来运行程序,观察执行结果:
从运行结果来看,符合预期。每个线程都获取到了锁。所以说,公平锁是对每一个等待获取锁的线程都是公平的。
6.释放锁操作应该放在finally代码块里面
建议大家把释放锁的操作放在finally代码块里面:
7.ReentrantLock(boolean fair)构造方法
回过头来,我们再来看看ReentrantLock类的ReentrantLock(boolean fair)构造方法长什么样:
将注释翻译成中文:
中文注释全文:
使用给定的公平策略创建ReentrantLock的实例。
去掉注释版:
参数fair的中文意思是公平。
当我们设置fair为true时,构造方法会创建一个公平同步对象:
FairSync类是一个静态内部类:
FairSync是一个公平锁的同步对象。它里面会将正在等待该同步对象的线程用队列记录下来。具体请看这个类:
上图Node类这就是记录每个正在等待锁的节点信息。
里面有排在它前面的节点:
还有排在它后面的节点:
以及当前节点的线程是谁:
如此一来,新的等待同步锁的线程加入的话记录在next节点即可。
如果是要取同步锁的话,我们只需判断当前节点的前面还有没有节点即可:
如果前面有节点的话,就不尝试获取锁了。本来tryAcquire(int acquires)方法就是尝试性获取同步锁,如果获取到同步锁返回true,否则返回false。