前言:对于一个控制锁的业务场景来说,有简单的也有复杂的,最简单的就是判断一个对象是否是null。再复杂点就是对于一个复杂条件的判断。 判断的话如果是一个boolean类型,guava提供了一个监视器类来实现, 相比传统java提供的ReentrantLock,synchronized,他提供了很大的便利性。好,我们一探窥见。
1、Monitor介绍
此类旨在代替ReentrantLock。与使用的代码相比,使用的代码Monitor 不易出错且可读性强ReentrantLock,而不会造成明显的性能损失。 Monitor通过优化条件的评估和信号传递,甚至具有提高性能的潜力。信令是完全 隐式的。通过消除显式的信号传递, 此类可以保证在条件变为真时不会唤醒一个线程(不会由于使用引起“信号风暴” Condition.signalAll), 并且不会丢失信号(由于对的不正确使用而不会导致“挂起” Condition.signal)。 在调用任何具有void返回类型的enter方法时,应始终紧随其后的是try / finally块,以确保当前线程干净地离开监视器:
// 实现就是包装了重入锁的lock.lock()
monitor.enter();
try {
// do things while occupying the monitor
} finally {
monitor.leave();
}
对任何带有boolean返回类型的enter方法的调用应始终作为包含try / finally块的if语句的条件出现,以确保当前线程干净地离开监视器:
// 实现就是包装了重入锁的lock.tryLock()
if (monitor.tryEnter()) {
try {
// do things while occupying the monitor
} finally {
monitor.leave();
}
} else {
// do other things since the monitor was not available
}
1、与synchronized、ReentrantLock比较
下面的例子显示使用表达一个简单的线程持有人synchronized, ReentrantLock和Monitor。
- synchronized
该版本是最少的代码行,主要是因为所使用的同步机制已内置在语言和运行时中。但是程序员必须记住要避免几个常见的错误:wait()必须在while而不是if,并且 notifyAll()必须使用,notify()因为必须等待两个不同的逻辑条件。
public class SafeBox<V> {
private V value;
public synchronized V get() throws InterruptedException {
while (value == null) {
wait();
}
V result = value;
value = null;
notifyAll();
return result;
}
public synchronized void set(V newValue) throws InterruptedException {
while (value != null) {
wait();
}
value = newValue;
notifyAll();
}
}
- ReentrantLock
该版本比synchronized版本更为冗长,并且仍然需要程序员记住要使用while而不是if。但是,一个优点是我们可以引入两个单独的Condition对象,这使我们可以使用signal()代替signalAll(),这可能会带来性能上的好处。
public class SafeBox<V> {
private final ReentrantLock lock = new ReentrantLock();
private final Condition valuePresent = lock.newCondition();
private final Condition valueAbsent = lock.newCondition();
private V value;
public V get() throws InterruptedException {
lock.lock();
try {
while (value == null) {
valuePresent.await();
}
V result = value;
value = null;
valueAbsent.signal();
return result;
} finally {
lock.unlock();
}
}
public void set(V newValue) throws InterruptedException {
lock.lock();
try {
while (value != null) {
valueAbsent.await();
}
value = newValue;
valuePresent.signal();
} finally {
lock.unlock();
}
}
}
- Monitor
此版本在Guard对象周围添加了一些详细信息,但从get和set方法中删除了相同的详细信息,甚至更多。 Monitor实现了与上述ReentrantLock版本中手动编码相同的有效信令。 最后,程序员不再需要手动编写等待循环的代码,因此不必记住要使用while代替if。
public class SafeBox<V> {
private final Monitor monitor = new Monitor();
private final Monitor.Guard valuePresent = new Monitor.Guard(monitor) {
public boolean isSatisfied() {
return value != null;
}
};
private final Monitor.Guard valueAbsent = new Monitor.Guard(monitor) {
public boolean isSatisfied() {
return value == null;
}
};
private V value;
public V get() throws InterruptedException {
monitor.enterWhen(valuePresent);
try {
V result = value;
value = null;
return result;
} finally {
monitor.leave();
}
}
public void set(V newValue) throws InterruptedException {
monitor.enterWhen(valueAbsent);
try {
value = newValue;
} finally {
monitor.leave();
}
}
}
2、Monitor原理
- 首先得了解下Monitor结构
private final boolean fair;
private final ReentrantLock lock;
private Guard activeGuards = null;
从上面结构可以看出来,Monitor也有公平非公平之分,因为他底层也是基于lock封装的,比较创新 的是有个activeGuards的Guard,那么得再仔细了解下Guard类。
- Guard类结构
final Monitor monitor;
final Condition condition;
int waiterCount = 0;
Guard next;
public abstract boolean isSatisfied();
警卫类是依赖一个monitor,没有monitor也就没有必要警卫了。 condition的作用就是关联一个锁条件,锁条件的实现是重写抽象方法isSatisfied。 waiterCount,意思是重入的次数,其实就是想知道是第一次还是最后一次,最后一次需要替换next指针。
结构看明白了,那么进入正题,看下如何做到加锁和写锁。
- Monitor加锁
已enterWhen为例:
public void enterWhen(Guard guard) throws InterruptedException {
// null判断,没什么好说的
if (guard.monitor != this) {
throw new IllegalMonitorStateException();
}
// 减少指针引用路径
final ReentrantLock lock = this.lock;
// 锁是否被当前线程持有
boolean signalBeforeWaiting = lock.isHeldByCurrentThread();
// 尝试获取锁
lock.lockInterruptibly();
boolean satisfied = false;
try {
// 警卫是否安全,不安全则等待
if (!guard.isSatisfied()) {
// 等待警卫通知
await(guard, signalBeforeWaiting);
}
satisfied = true;
} finally {
if (!satisfied) {
leave();
}
}
}
private void await(Guard guard, boolean signalBeforeWaiting) throws InterruptedException {
// 等待是否先通知,当前线程已经拿到锁了,进行看下一个等待对象
if (signalBeforeWaiting) {
signalNextWaiter();
}
// 第一次开始等待,就是记录下waiterCount
beginWaitingFor(guard);
try {
do {
// 第一次开始await
guard.condition.await();
// 看条件,其实和那种最普通的写法是一样的
} while (!guard.isSatisfied());
} finally {
// 记录下waiterCount,判断是否需要执行next警卫
endWaitingFor(guard);
}
}
private void signalNextWaiter() {
for (Guard guard = activeGuards; guard != null; guard = guard.next) {
if (isSatisfied(guard)) {
guard.condition.signal();
break;
}
}
}
private void beginWaitingFor(Guard guard) {
int waiters = guard.waiterCount++;
if (waiters == 0) {
// push guard onto activeGuards
guard.next = activeGuards;
activeGuards = guard;
}
}
private void endWaitingFor(Guard guard) {
int waiters = --guard.waiterCount;
if (waiters == 0) {
// unlink guard from activeGuards
for (Guard p = activeGuards, pred = null; ; pred = p, p = p.next) {
if (p == guard) {
if (pred == null) {
activeGuards = p.next;
} else {
pred.next = p.next;
}
p.next = null; // help GC
break;
}
}
}
}
- Monitor解锁
解锁相对加锁步骤少了很多,finally里面进行unlock释放锁
/**
* Leaves this monitor. May be called only by a thread currently occupying this monitor.
*/
public void leave() {
final ReentrantLock lock = this.lock;
try {
// No need to signal if we will still be holding the lock when we return
if (lock.getHoldCount() == 1) {
signalNextWaiter();
}
} finally {
lock.unlock(); // Will throw IllegalMonitorStateException if not held
}
}
写在最后
这里就简单分析下Monitor的实现了,点到为止,可以看出通过抽象Monitor和Guard,把锁条件进行封装,有点策略和单个责任链模式的意思, 这么想可能是google程序员觉得jdk的lock还是不够抽象,所以再封装了一层。
写这篇文章也就花了半个多小时的时间,发现3篇文章一写确实越来越顺了,也有可能分析的还是过于表面,但是确实写完比看完一个东西能理解更深入。
这里感觉有个学习深度的总结还真有道理。
知识学习的层次是:看懂 < 说出来 < 写出来并能让别人也懂
扫描二维码,关注公众号“猿必过”
回复 “面试题” 自行领取吧。
微信群交流讨论,请添加微信号:zyhui98,备注:面试题加群
本文由猿必过 YBG 发布 禁止未经授权转载,违者依法追究相关法律责任 如需授权可联系:zhuyunhui@yuanbiguo.com