学习ReentrantLock之前,先了解一下可重入锁的概念。何为可重入锁,顾名思义,就是可重入的。真是听君一席话,胜听一席话啊。
正经点,可重入锁就是能够支持同一个线程对资源的重复加锁。注意两个关键字:同一线程和重复。
像synchronized关键字也实现了可重入。用synchronized修饰的方法,在进行递归调用时,执行线程在获取了锁之后仍然能够连续多次获得该锁,并不会出现阻塞的情况。
再比如说,这篇文章要学习的ReentrantLock
,也实现了可重入锁。并且ReentrantLock
还支持公平锁和非公平锁(默认是非公平锁)。
1、ReentrantLock源码学习
1.1 构造方法
ReentrantLock
的源码比较简单,并且它也是基于AQS实现的。先看看它的构造函数
1 | java复制代码 /** |
默认就是非公平锁。
1.2 锁的释放
Sync
类就是继承自AQS的,FairSync
类和NonfairSync
类又是继承自Sync
。对于公平锁和非公平锁,其释放锁的逻辑都是一样的,所以在Sync
类中实现。
1 | java复制代码 abstract static class Sync extends AbstractQueuedSynchronizer { |
可以发现可重入锁的释放逻辑,对于占有锁的线程来说,只有在同步变量state的值为0的时候,才算是释放了锁。
1.3 锁的获取
锁的获取分公平锁和非公平锁。非公平锁的获取逻辑实现在Sync
类中
1 | java复制代码abstract static class Sync extends AbstractQueuedSynchronizer { |
可以发现Sync
类似并没有重写AQS的tryAcquire
方法,而是放到了它的子类FairSync
类和NonfairSync
类中去实现的。
看看NonfairSync
类的源码
1 | java复制代码 // 非公平锁 |
看看FairSync
类的源码
1 | java复制代码 // 公平锁 |
可以发现公平锁和非公平锁在获取锁的时候,唯一的差别就是公平锁判断了当前节点是不是头结点,只有是头结点的情况下才可能获取到锁。非公平锁就不一样了,上来就直接CAS。
上面的方法都是在ReentrantLock
类内部用的,对外提供的接口如下
1 | java复制代码// 获取锁,在等待获取锁的过程中休眠并禁止一切线程调度 |
2、测试
测试一下ReentrantLock
的公平锁和非公平锁。
1 | java复制代码import java.util.ArrayList; |
公平锁输出如下
1 | java复制代码locked by 0, waiting by [1, 2] |
非公平锁输出如下
1 | java复制代码locked by 0, waiting by [2] |
可以发现公平锁总是按照顺序来依次获取锁。而非公平锁却是连续获取。回顾nonfairTryAcquire(int acquires)
方法,当一 个线程请求锁时,只要获取了同步状态即成功获取锁。在这个前提下,刚释放锁的线程再次获取同步状态的几率会非常大,使得其他线程只能在同步队列中等待。
非公平锁可能会出现线程饥饿的情况,当竞争的线程很多时,后面的线程可能一直都获取不到锁。那为啥ReentrantLock
默认是非公平锁呢?经过上面的测试可以发现,在公平锁的情况下,线程进行了10次上下文切换,非公平锁情况下只进行了5次。
线程上下文切换是一个耗费时间和资源的操作,所以在线程竞争激烈的情况下,非公平锁无疑能够节省很多的资源。
总结一下就是:公平性锁保证了锁的获取按照FIFO原则,而代价是进行大量的线程切换。非公平性锁虽然可能造成线程“饥饿”,但极少的线程切换,保证了其更大的吞吐量。
本文转载自: 掘金