JAVA各种锁的优劣对比分析( 五 )


但是对于非公平锁 , 管理员对打水的人没有要求 。即使等待队伍里有排队等待的人 , 但如果在上一个人刚打完水把锁还给管理员而且管理员还没有允许等待队伍里下一个人去打水时 , 刚好来了一个插队的人 , 这个插队的人是可以直接从管理员那里拿到锁去打水 , 不需要排队 , 原本排队等待的人只能继续等待 。如下图所示:

JAVA各种锁的优劣对比分析

文章插图
 
接下来我们通过ReentrantLock的源码来讲解公平锁和非公平锁 。
JAVA各种锁的优劣对比分析

文章插图
 
根据代码可知 , ReentrantLock里面有一个内部类Sync , Sync继承AQS(AbstractQueuedSynchronizer) , 添加锁和释放锁的大部分操作实际上都是在Sync中实现的 。它有公平锁FairSync和非公平锁NonfairSync两个子类 。ReentrantLock默认使用非公平锁 , 也可以通过构造器来显示的指定使用公平锁 。
下面我们来看一下公平锁与非公平锁的加锁方法的源码:
JAVA各种锁的优劣对比分析

文章插图
 
通过上图中的源代码对比 , 我们可以明显的看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors() 。
JAVA各种锁的优劣对比分析

文章插图
 
再进入hasQueuedPredecessors() , 可以看到该方法主要做一件事情:主要是判断当前线程是否位于同步队列中的第一个 。如果是则返回true , 否则返回false 。
综上 , 公平锁就是通过同步队列来实现多个线程按照申请锁的顺序来获取锁 , 从而实现公平的特性 。非公平锁加锁时不考虑排队等待问题 , 直接尝试获取锁 , 所以存在后申请却先获得锁的情况 。
5. 可重入锁 VS 非可重入锁可重入锁又名递归锁 , 是指在同一个线程在外层方法获取锁的时候 , 再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class) , 不会因为之前已经获取过还没释放而阻塞 。Java中ReentrantLock和synchronized都是可重入锁 , 可重入锁的一个优点是可一定程度避免死锁 。下面用示例代码来进行分析:
JAVA各种锁的优劣对比分析

文章插图
 
在上面的代码中 , 类中的两个方法都是被内置锁synchronized修饰的 , doSomething()方法中调用doOthers()方法 。因为内置锁是可重入的 , 所以同一个线程在调用doOthers()时可以直接获得当前对象的锁 , 进入doOthers()进行操作 。
如果是一个不可重入锁 , 那么当前线程在调用doOthers()之前需要将执行doSomething()时获取当前对象的锁释放掉 , 实际上该对象锁已被当前线程所持有 , 且无法释放 。所以此时会出现死锁 。
而为什么可重入锁就可以在嵌套调用时可以自动获得锁呢?我们通过图示和源码来分别解析一下 。
还是打水的例子 , 有多个人在排队打水 , 此时管理员允许锁和同一个人的多个水桶绑定 。这个人用多个水桶打水时 , 第一个水桶和锁绑定并打完水之后 , 第二个水桶也可以直接和锁绑定并开始打水 , 所有的水桶都打完水之后打水人才会将锁还给管理员 。这个人的所有打水流程都能够成功执行 , 后续等待的人也能够打到水 。这就是可重入锁 。
JAVA各种锁的优劣对比分析

文章插图
 
但如果是非可重入锁的话 , 此时管理员只允许锁和同一个人的一个水桶绑定 。第一个水桶和锁绑定打完水之后并不会释放锁 , 导致第二个水桶不能和锁绑定也无法打水 。当前线程出现死锁 , 整个等待队列中的所有线程都无法被唤醒 。
JAVA各种锁的优劣对比分析

文章插图
 
之前我们说过ReentrantLock和synchronized都是重入锁 , 那么我们通过重入锁ReentrantLock以及非可重入锁NonReentrantLock的源码来对比分析一下为什么非可重入锁在重复调用同步资源时会出现死锁 。
首先ReentrantLock和NonReentrantLock都继承父类AQS , 其父类AQS中维护了一个同步状态status来计数重入次数 , status初始值为0 。


推荐阅读