jdk 源码系列之ReentrantLock
Posted 编程的那些年
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了jdk 源码系列之ReentrantLock相关的知识,希望对你有一定的参考价值。
最近将 ReentrantLock 学了一遍同时也把源码读了一遍,记录下学习的过程
JDK 源码系列
jdk 源码系列之StringBuilder、StringBuffer
jdk 源码系列之HashMap
使用
使用锁机制,来保障线程安全
Lock lock = new ReentrantLock();
lock.lock();
try {
// 受此锁保护的资源块
} finally {
lock.unlock();
}
或者你可以使用 tryLock() 方法,在多线程中,当一个线程释放锁的时候,就尝试去获取锁。
Lock lock = new ReentrantLock();
if (lock.tryLock()) {
try {
// 受此锁保护的资源块
} finally {
lock.unlock();
}
} else {
// 进行其他操作,未被锁保护
}
这种方法,在确保获取锁的时候才会解锁,并且在未获锁时不会尝试去解锁。
如果尝试去获取锁的时候太长,也可以给获取锁的这个过程加上时间,超时则直接中断线程。
public static void main(String[] args) throws InterruptedException {
Lock lock = new ReentrantLock();
if (lock.tryLock(5, TimeUnit.SECONDS)) {
try {
// manipulate protected state
}
finally {
lock.unlock();
}
} else {
// perform alternative actions
}
}
}
如果希望当前锁的模块不是立刻执行,也可以调用 await 机制
Lock lock = new ReentrantLock();
lock.lock();
try {
// manipulate protected state
lock.newCondition().await(5, TimeUnit.SECONDS);
}
finally {
lock.unlock();
}
有时候,当遇到过长的业务流程,导致持有的时间太长了,可以考虑打断锁的机制,释放锁。
Lock lock = new ReentrantLock();
lock.lock();
try {
// manipulate protected state
long startTime = System.currentTimeMillis();
long endTime = System.currentTimeMillis();
if (endTime - startTime > 10) {
lock.lockInterruptibly();
try {
} finally {
lock.unlock();
}
}
lock.newCondition().await(5, TimeUnit.SECONDS);
}
finally {
lock.unlock();
}
应用场景比较
方法 | 说明 | 适合场景 | 注意事项 |
---|---|---|---|
lock | 获取锁 | 保证顺序,同步进行 | 必须小心以确保通过try-finally或try-catch保护持有锁定时执行的所有代码,以确保在必要时释放锁定 |
tryLock | 仅在调用时释放锁时才获取锁 | 在没有锁的情况下,可以去做别的事情,充分利用线程 | 如果加上了获取时间,必须记录情况和异常类型 |
lockInterruptibly | 除非当前线程被中断,否则获取锁 | 业务时间过长,持有的锁太久,可以直接中断 | 必须记录情况和异常类型 |
newCondition | 绑定条件在锁上 | 实现一个延迟加锁机制 | 必须记录情况 |
unlock | 释放锁 | 只要使用到了锁,最终都要必须释放锁 | 只要使用上了锁,必须使用到该方法 |
源码
先看类
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
向上继承了 Lock 接口,以及 Serializable,都是实现了 Lock 的方法。
void lock() 获取锁
void lockInterruptibly() 中断锁机制
boolean tryLock() 其他线程有释放锁,才调用获取锁
boolean tryLock(long time, TimeUnit unit) 给定时间内线程是空闲时间,且其他线程有释放锁,才调用获取锁
void unlock() 释放锁
Condition newCondition() 给锁绑定条件
实例化锁
Lock lock = new ReentrantLock();
Lock lock1 = new ReentrantLock(false);
Lock lock2 = new ReentrantLock(true);
这里,有两个构造,一个是无参构造,一个是传入布尔值。
/**
* Creates an instance of {@code ReentrantLock}.
* This is equivalent to using {@code ReentrantLock(false)}.
*/
public ReentrantLock() {
// 创建一个非公平锁
sync = new NonfairSync();
}
/**
* Creates an instance of {@code ReentrantLock} with the
* given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantLock(boolean fair) {
// true 公平锁 false 非公平锁
sync = fair ? new FairSync() : new NonfairSync();
}
这里默认是构造一个非公平锁,也可以直接设置创建什么类型锁,比如公平锁、非公平锁。
我们先看看公平锁。
/**
* Sync object for fair locks
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
// 当使用 lock.lock() 的时候调到 同时传入1
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
// 这里 acquires = 1指的是 上锁的状态 0则是未上锁
protected final boolean tryAcquire(int acquires) {
// 获取当前线程对象
final Thread current = Thread.currentThread();
// 是否上锁,首次默认是0
int c = getState();
if (c == 0) {
// hasQueuedPredecessors 是否是当前线程,使用了双向链表的数据结构,做一个 queue,这里的意思大概是到了这个线程可以加锁了。不用排队了
if (!hasQueuedPredecessors() &&
// CAS 比较后修改成功
compareAndSetState(0, acquires)) {
// 将当前线程设置成独占线程
setExclusiveOwnerThread(current);
// 获取锁成功
return true;
}
}
// 独占线程
else if (current == getExclusiveOwnerThread()) {
// nextc = 1
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 同步锁的状态 = 1
setState(nextc);
return true;
}
// 其他线程则未获取到锁
return false;
}
}
/**
* Acquires in exclusive mode, ignoring interrupts. Implemented
* by invoking at least once {@link #tryAcquire},
* returning on success. Otherwise the thread is queued, possibly
* repeatedly blocking and unblocking, invoking {@link
* #tryAcquire} until success. This method can be used
* to implement method {@link Lock#lock}.
*
* @param arg the acquire argument. This value is conveyed to
* {@link #tryAcquire} but is otherwise uninterpreted and
* can represent anything you like.
*/
public final void acquire(int arg) {
// 尝试上锁
if (!tryAcquire(arg) &&
// 先将线程当前线程添加进队列最后一个,然后在判断是否到他上锁了
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 没有则直接打断线程
selfInterrupt();
}
/**
* Acquires in exclusive uninterruptible mode for thread already in
* queue. Used by condition wait methods as well as acquire.
*
* @param node the node
* @param arg the acquire argument
* @return {@code true} if interrupted while waiting
*/
// 持有锁的队列里面的线程
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
// 一个死循环你判断里面当前谁获得锁
for (;;) {
// 指向下一个节点
final Node p = node.predecessor();
// 队列的第一个,且获取锁,将当前线程变成独占线程
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
// 如果获得到锁,则不需要打断当前线程返回一个 false
return interrupted;
}
// 线程阻塞了
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 中断获取锁的机制
interrupted = true;
}
} finally {
// 如果获得到锁 则不需要取消加锁操作、反之则取消
if (failed)
cancelAcquire(node);
}
}
公平锁,当前的线程会进入一个队列中最后一个,等待到他的上锁。如果当前的线程表示获取到锁,且也是队列的第一个位置。会将自己变成锁的独占线程,只有自己才持有这个锁的对象。同时这里进行了 CAS 的原子交换,设置状态为1。其中 0 为 未上锁状态,1为上锁状态。
非公平锁
/**
* Sync object for non-fair locks
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
// 先进行抢占锁,如果如可以修改状态,则直接变成上锁状态
if (compareAndSetState(0, 1))
// 设置独占线程
setExclusiveOwnerThread(Thread.currentThread());
else
// 加锁
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
/**
* Performs non-fair tryLock. tryAcquire is implemented in
* subclasses, but both need nonfair try for trylock method.
*/
final boolean nonfairTryAcquire(int acquires) {
// 这个和公平锁里面差不多
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
非公平锁,先进行抢占锁,先查看线程是否释放了锁,如果释放了,当前线程则直接上锁,这样的好处就是减少了线程之间的等待,加快了上锁的机制,避免了排队时竞争所导致的延时,提高了性能,如果没有释放锁,则进入到队列的最后一个等待上锁。
小结一下。
锁的类型 | 机制 | 效率 |
---|---|---|
公平锁 | 将线程放进队列的最后一个,等待上锁 | 一般 |
非公平 | 先进行抢占锁,获取到锁则直接上锁,没有则将线程放进队列的最后一个等待上锁 | 较高 |
释放锁机制。
/**
* Attempts to release this lock.
*
* <p>If the current thread is the holder of this lock then the hold
* count is decremented. If the hold count is now zero then the lock
* is released. If the current thread is not the holder of this
* lock then {@link IllegalMonitorStateException} is thrown.
*
* @throws IllegalMonitorStateException if the current thread does not
* hold this lock
*/
public void unlock() {
// 携带 1
sync.release(1);
}
public final boolean release(int arg) {
// 释放成功则为 true 否则是 false
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
// 唤醒队列后面的线程
unparkSuccessor(h);
// 释放成功
return true;
}
// 释放失败
return false;
}
protected final boolean tryRelease(int releases) {
// c = 0
int c = getState() - releases;
// 如果不是当前线程和不是独占线程
if (Thread.currentThread() != getExclusiveOwnerThread())
// 直接抛出错
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
// 释放锁
free = true;
// 独占线程设置null
setExclusiveOwnerThread(null);
}
// 0
setState(c);
return free;
}
锁的释放,比较简单。将锁状态重新设置回 0,同时独占线程也设置null,之后唤醒后面的队列里面的线程,完成释放。
源码总结
ReentrantLock 创建的时候,默认是非公平锁,不过你也可以在构造的时候,也可以创建一个公平锁。 其中通过 CAS 改变 state 的状态来改变锁的数值, 0 表示有锁可以获取,1 表示锁已被获取,来设置锁的独占线程。
在公平锁的机制中,请求锁的线程会直接排到一个队列中(通过一个双向链表来模拟的队列)的最后一个,去获取锁。
非公平锁的机制中,请求锁的线程首先会先通过 CAS 来改变 state 的锁状态,如果可以改变(0 -> 1),则直接获取到锁,将自身设置成独占锁。这样的好处就减少了一些进队列、加载队列、唤醒线程等性能消耗。如果未能修改到 state 的状态,也会变成公平锁的机制,进入到队列的最后一个,等待到它去获取锁。
锁的释放,将 state 重新设置回 0,同时独占线程(你也可以认为这是持有锁的线程对象)设置null,之后唤醒排在它下个的线程。这一系列步骤做完,则宣告锁的释放。
优缺点
非公平锁,确实性能比较高。不过也有一个显而易见的缺点,我们可以想象一下,当你在排队吃饭的时候,轮到你吃饭的时候,这时候突然来一个人插在你前面,提前打饭了,导致你打饭时间变长了,如果这时候在有几个人在也突然插到你前打饭,又会继续导致你打饭时间变得更长。那如果放到线程里面,突然其他线程提前获取到了锁,那会导致当前线程获取到锁时间变长,而导致线程阻塞,迟迟未获取到锁。
所以得根据业务去选择合适的锁类型,进行上锁,尽可能的避免有一些重要的业务因为上锁而阻塞到。
引用
[1] Lock (Java Platform SE 8 )
以上是关于jdk 源码系列之ReentrantLock的主要内容,如果未能解决你的问题,请参考以下文章
JDK类库源码分析系列2-AbstractQueuedSynchronizer-ReentrantReadWriteLock