哈喽,大家好,我是指北君。
今天为你带来的是 ReentrantLock 公平锁与非公平锁的分析,它是 Java 并发包下的一个实现类,实现了 Lock 接口和 Serializable 接口。
初识
ReentrantLock 类有两个构造函数,一个是默认的不带参数的构造函数,创建一个默认的非公平锁的实现,一个是带参数的构造函数,根据参数 fair 创建一个公平还是非公平的锁。
1 |
|
这里简单的说一下公平锁和非公平锁的定义:
- 公平锁:线程在同步队列中通过先进先出(FIFO)的方式获取锁,每个线程最终都能获取锁。
- 非公平锁:线程可以通过插队的方式抢占锁,抢不到锁就进入同步队列排队。
NonfairSync 类和 FairSync 类继承了 Sync 类,它们三个都是 ReentrantLock 的内部类。
AbstractQueuedSynchronizer,简称 AQS,拥有三个核心组件:
- state:volatile 修饰,线程是否可以获取锁。
- Node:内部队列,双向链表形式,没有抢到锁的对象就进入这个队列。 主要字段有:pre 前一个节点,next 下一个节点,thread 线程,waitStatus 线程的状态。
- exclusiveOwnerThread:当前抢到锁的线程。
如下图,简单的了解一下 AQS。
1 |
|
公平锁
在分析公平锁之前,先介绍一下 Sync 类,它是 ReentrantLock 的唯一的属性,在构造函数中被初始化,决定了用公平锁还是非公平锁的方式获取锁。
1 |
|
用以下构造方法创建一个公平锁。
1 |
|
沿着 lock.lock()
调用情况一路往下分析。
1 |
|
acquire() 方法主要是干了 3 件事情
- tryAcquire() 尝试获取锁。
- 获取锁失败后,调用 addWaiter() 方法将线程封装成 Node,加入同步队列。
- acquireQueued() 将队列中的节点按自旋的方式尝试获取锁。
1 |
|
tryAcquire() 尝试获取锁,如果线程本身持有锁,则将这个线程重入锁。
1 |
|
hasQueuedPredecessors() 这个方法比较有绅士风度,在 tryAcquire() 方法中被第一个调用,它谦让比自己排队长的线程。
1 |
|
addWaiter() 方法就是将获取锁失败的线程加入到同步队列尾部。
1 |
|
acquireQueued() 方法当节点为首节点的时候,再次调用 tryAcquire() 获取锁,否则就阻塞线程,等待被唤醒。
1 |
|
shouldParkAfterFailedAcquire() 线程是否需要被阻塞,更改线程的 waitStatus 为 SIGNAL。parkAndCheckInterrupt() 实现真正的阻塞线程。
1 |
|
以上就是公平锁获取锁的全部过程,总结一下公平锁获取锁的过程:
- 当前线程调用 tryAcquire() 获取锁,成功则返回。
- 调用 addWaiter(),将线程封装成 Node 节点加入同步队列。
- acquireQueued() 自旋尝试获取锁,成功则返回。
- shouldParkAfterFailedAcquire() 将线程设置为等待唤醒状态,阻塞当前线程。
- 如果线程被唤醒,尝试获取锁,成功则返回,失败则继续阻塞。
非公平锁
用默认的构造方式创建一个非公平锁。lock() 方法上来就尝试抢占锁,失败则调用 acquire() 方法。
1 |
|
nonfairTryAcquire() 就没有绅士风度了,没有了公平锁 hasQueuedPredecessors() 方法。
1 |
|
以上就是非公平锁获取锁,总结一下非公平锁获取锁的过程:
- lock() 第一次尝试获取锁,成功则返回。
- nonfairTryAcquire() 再次尝试获取锁。
- 失败则调用 addWaiter() 封装线程为 Node 节点加入同步队列。
- acquireQueued() 自旋尝试获取锁,成功则返回。
- shouldParkAfterFailedAcquire() 将线程设置为等待唤醒状态,阻塞当前线程。
- 如果线程被唤醒,尝试获取锁,成功则返回,失败则继续阻塞。
公平锁和非公平锁对比
在下图源码中可以看出,公平锁多了一个 !hasQueuedPredecessors()
用来判断是否有其他线程比当前线程在同步队列中排队时间更长。除此之外,非公平锁在初始时就有 2 次获取锁的机会,然后再到同步队列中排队。
unlock() 释放锁
获取锁之后必须得释放,同一个线程不管重入了几次锁,必须得释放几次锁,不然 state 变量将不会变成 0,锁被永久占用,其他线程将永远也获取不到锁。
1 |
|
释放锁的逻辑就是 state 必须被减去 1 直到为 0,才可以唤醒下一个线程。
总结
ReentrantLock 主要是防止资源的使用冲突,保证同一个时间只能有一个线程在使用资源。比如:文件操作,同步发送消息等等。
本文分析了 ReentrantLock 的公平锁和非公平锁以及释放锁的原理,可以得出非公平锁的效率比公平锁效率高,非公平锁初始时会 2 次获取锁,如果成功可以减少线程切换带来的损耗。在非公平模式下,线程可能一直抢占不到锁。
我是指北君,操千曲而后晓声,观千剑而后识器。感谢各位人才的:点赞、收藏和评论,我们下期更精彩!