文前说明
作为码农中的一员,需要不断的学习,我工作之余将一些分析总结和学习笔记写成博客与大家一起交流,也希望采用这种方式记录自己的学习之旅。
本文仅供学习交流使用,侵权必删。
不用于商业目的,转载请注明出处。
1. ReentrantLock
-
ReentrantLock
即可重入锁,实现了 Lock 和 Serializable 接口。 - 在 Java 环境下
ReentrantLock
和synchronized
都是可重入锁。- 可重入锁,也叫做递归锁,当一个线程请求得到一个对象锁后再次请求此对象锁,可以再次得到该对象锁。
- 调用
get()
方法,同一个线程 ID 会被连续输出两次。
public void get() {
lock.lock();
System.out.println(Thread.currentThread().getId());
set();
lock.unlock();
}
public void set() {
lock.lock();
System.out.println(Thread.currentThread().getId());
lock.unlock();
}
-
ReentrantLock
构造函数中提供了两种锁:创建公平锁和非公平锁(默认)。- RentrantLock 有三个内部类 Sync、NonfairSync 和 FairSync 类。
- Sync 继承 AbstractQueuedSynchronizer 抽象类。
- NonfairSync(非公平锁) 继承 Sync 抽象类。
- FairSync(公平锁) 继承 Sync 抽象类。
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
- 在公平的锁上,线程按照他们发出请求的顺序获取锁,但在非公平锁上,则允许插队。
- 当一个线程请求非公平锁时,如果在发出请求的同时该锁变成可用状态,那么这个线程会跳过队列中所有的等待线程而获得锁。
- 非公平的
ReentrantLock
并不提倡插队行为,但是无法防止某个线程在合适的时候进行插队。
- 在公平的锁上,如果有另一个线程持有锁或者有其他线程在等待队列中等待这个锁,那么新发出的请求的线程将被放入到队列中。
- 而非公平锁上,只有当锁被某个线程持有时,新发出请求的线程才会被放入队列中。
- 非公平锁性能高于公平锁性能的原因
- 在恢复一个被挂起的线程与该线程真正运行之间存在着严重的延迟。
- 假设线程 A 持有一个锁,并且线程 B 请求这个锁。由于锁被 A 持有,因此 B 将被挂起。当 A 释放锁时,B 将被唤醒,因此 B 会再次尝试获取这个锁。与此同时,如果线程 C 也请求这个锁,那么 C 很可能会在 B 被完全唤醒之前获得、使用以及释放这个锁。这样就是一种双赢的局面,B 获得锁的时刻并没有推迟,C 更早的获得了锁,并且吞吐量也提高了。
- 当持有锁的时间相对较长或者请求锁的平均时间间隔较长,应该使用公平锁。
- 在这些情况下,插队带来的吞吐量提升(当锁处于可用状态时,线程却还处于被唤醒的过程中)可能不会出现。
1.1 公平锁与非公平锁实现
公平锁
- 公平锁表示线程获取锁的顺序是按照 线程加锁的顺序 来分配的,即先来先得的 FIFO 先进先出顺序。
- 每个线程获取锁的过程是公平的,等待时间最长的会最先被唤醒获取锁。
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
acquire(1);
}
/**
* Fair version of tryAcquire. Don't grant access unless
* recursive call or no waiters or is first.
*/
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
- 公平锁
tryAcquire()
方法比非公平锁nonfairTryAcquire()
方法多了一个hasQueuedPredecessors()
方法。- hasQueuedPredecessors 方法判断了头结点是否为当前线程。
public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
非公平锁
- 而非公平锁就是一种获取锁的抢占机制,是 随机获得锁 的,和公平锁不一样的就是先来的不一定先得到锁,这个方式可能造成某些线程一直拿不到锁,结果也就是不公平的了。
- 非公平锁可能使线程 " 饥饿 "。
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);
}
}
- 是否公平只是针对入队前,入队后都是按顺序获取锁。
1.2 重入锁实现
- 线程可以重复获取已经持有的锁。
- 在非公平和公平锁中,都对重入锁进行了实现。
if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
1.3 使用 ReentrantLock 场景
场景 1:如果发现该操作已经在执行中则不再执行(有状态执行)
private ReentrantLock lock = new ReentrantLock();
if (lock.tryLock()) { //如果已经被 lock,则立即返回 false 不会等待,达到忽略操作的效果。
try {
//操作
} finally {
lock.unlock();
}
}
场景 2:如果发现该操作已经在执行,等待一个一个执行(同步执行,类似 synchronized)
private ReentrantLock lock = new ReentrantLock(); //参数默认false,不公平锁
// private ReentrantLock lock = new ReentrantLock(true); //公平锁
try {
lock.lock(); //如果被其它资源锁定,会在此等待锁释放,达到暂停的效果
//操作
} finally {
lock.unlock();
}
- 公平情况下,操作会排一个队按顺序执行,来保证执行顺序。(会消耗更多的时间来排队)
- 不公平情况下,是无序状态允许插队,JVM 会自动计算如何处理更快速来调度插队。(如果不关心顺序,这个速度会更快)
场景 3:如果发现该操作已经在执行,则尝试等待一段时间,等待超时则不执行(尝试等待执行)
try {
if (lock.tryLock(5, TimeUnit.SECONDS)) { //如果已经被 lock,尝试等待 5s,看是否可以获得锁,如果 5s 后仍然无法获得锁则返回 false 继续执行
try {
//操作
} finally {
lock.unlock();
}
}
} catch (InterruptedException e) {
e.printStackTrace(); //当前线程被中断时(interrupt),会抛 InterruptedException
}
场景 4:如果发现该操作已经在执行,等待执行。这时可中断正在进行的操作立刻释放锁继续下一操作。
try {
lock.lockInterruptibly();
//操作
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
- 这种情况主要用于取消某些操作对资源的占用。如:(取消正在同步运行的操作,来防止不正常操作长时间占用造成的阻塞),该操作的开销也很大,一般不建议使用。
场景 5:条件判断。
- 每一个 lock 可以有任意数据的 Condition 对象。
Lock lock = new ReentrantLock();
Condition condition = lock.newCondition();
lock.lock();
try {
while(条件判断表达式) {
condition.wait();
}
// 处理逻辑
} finally {
lock.unlock();
}
1.4 ReentrantLock 的方法
方法 | 说明 |
---|---|
getHoldCount() | 查询当前线程获取此锁的次数,此线程执行 lock 方法的次数。 |
getQueueLength() | 返回正等待获取此锁的线程估计数,比如启动 10 个线程,1 个线程获得锁,此时返回的是 9。 |
getWaitQueueLength(Condition condition) | 返回等待与此锁相关的给定条件的线程估计数。比如 10 个线程,用同一个 condition 对象,并且此时这 10 个线程都执行了 condition 对象的 await 方法,那么此时执行此方法返回 10。 |
hasWaiters(Condition condition) | 查询是否有线程等待与此锁有关的给定条件(condition),对于指定 contidion 对象,有多少线程执行了 condition.await 方法。 |
hasQueuedThread(Thread thread) | 查询给定线程是否等待获取此锁。 |
hasQueuedThreads() | 是否有线程等待此锁。 |
isFair() | 该锁是否公平锁。 |
isHeldByCurrentThread() | 当前线程是否保持锁锁定,线程的执行 lock 方法的前后分别是 false 和 true。 |
isLock() | 此锁是否有任意线程占用。 |
lockInterruptibly() | 如果当前线程未被中断,获取锁。 |
tryLock() | 尝试获得锁,仅在调用时锁未被线程占用,获得锁。 |
tryLock(long timeout, TimeUnit unit) | 如果锁在给定等待时间内没有被另一个线程获取,则获取该锁。 |
1.5 tryLock、lock 和 lockInterruptibly 的区别
- tryLock 能获得锁就返回 true,不能就立即返回 false。
- tryLock(long timeout,TimeUnit unit),可以增加时间限制,如果超过该时间段还没获得锁,返回 false。
- lock 能获得锁就返回 true,不能的话一直等待获得锁。
- lock 和 lockInterruptibly,如果两个线程分别执行这两个方法,但此时中断这两个线程,前者不会抛出异常,而后者会抛出异常。
1.6 总结
- ReentrantLock 提供了内置锁(synchronized)类似的功能和内存语义。
- ReentrantLock 提供了包括定时的锁等待、可中断的锁等待、公平性以及实现非块结构的加锁。
- Condition 对线程的等待和唤醒等操作更加灵活,一个 ReentrantLock 可以有多个 Condition 实例,更有扩展性。
- ReentrantLock 需要显示的获取锁,并在 finally 中释放锁。
- JDK 1.8 以前 ReentrantLock 在性能上似乎优于 synchronized,但获取锁的操作不能与特定的栈帧关联起来,而内置锁可以。
- 因为内置锁时 JVM 的内置属性,所以未来更可能提升 synchronized 而不是 ReentrantLock 的性能。
- 例如对线程封闭的锁对象消除优化,通过增加锁粒度来消除内置锁的同步。