熟悉 Java 并发的朋友,想必都会对 Lock 接口很熟悉,它是从 JDK1.5 以后提供给开发者的另一种线程同步的方式。下面是使用它的一个具体实现类:ReentrantLock 进行加锁解锁的一个小例子:
Lock lock = new ReentrantLock();
lock.lock();
try {
// access the resource protected by this lock
} finally {
lock.unlock();
}
上述这个段代码,其实也是下面链接中, JDK 的 document 举的使用示例。
https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/Lock.html
我们注意到,lock.unlock();
是被放入 finally 代码块里的,这是为了保证出现异常时,锁依然能被释放掉,避免死锁的产生。
我们还要注意到,加锁的过程:lock.lock();
,并没有放在 try 代码块内,而且你会发现,JDK 文档中很多使用 lock 的地方都是将加锁过程:lock.lock();
放在了 try 的外部。
但是今天发现网上有一些文章,给出的代码示例中,lock.lock();
被放在了 try 代码块的内部,如下所示:
Lock lock = new ReentrantLock();
try {
lock.lock();
// access the resource protected by this lock
} finally {
lock.unlock();
}
这样做合理吗?首先给出答案,千万不要这样做。。。
为什么呢?假设现在我们使用上述的实现,将lock.lock();
放在了 try 的内部。
考虑一种情况,如果说在获取锁时发生了异常,那么肯定也会走 finally 代码块,执行lock.unlock();
去释放锁,可问题是我还没获取到锁啊!!!
那么会发生什么呢?我们从源码的角度来分析,首先来看 ReentrantLock 的 unlock() 方法。
public void unlock() {
sync.release(1);
}
sync 是继承自抽象类 AbstractQueuedSynchronizer 的一个自定义同步器框架,AbstractQueuedSynchronizer 呢,也就是俗称的 AQS,它是 J.U.C 包的核心。
unlock() 首先会调用 sync 的 release() 方法,我们接着往下看这个方法。
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
release() 这个方法呢,其实是由 AQS 实现的,被 final 修饰,不允许被重写,它实际上会先去调用 tryRelease() 方法,而 tryRelease() 这个方法就是自定义同步框架时必须要重写的方法之一了,我们可以先看一下 AQS 中的 tryRelease() 方法源码。
protected boolean tryRelease(int arg) {
throw new UnsupportedOperationException();
}
被 protected 修饰,很明显就是让子类去 Override 的,而且方法体内只有一句抛出异常的代码,所以这个 tryRelease() 方法是必须重写的。我们接下来来看看 ReentrantLock 的重写后的 tryRelease() 方法。这里为了方便大家抓住重点,我直接截图不贴代码了。
注意我圈红的这段代码,tryRelease() 方法会判断当前线程是否是持有锁的线程!如果不是,那么它会抛出 IllegalMonitorStateException!
好了,我们可以回到正题了,现在我们明白了,在 try-finally 外加锁的话,如果因为发生异常导致加锁失败,try-finally 块中的代码不会执行。相反,如果在 try{ } 代码块中加锁失败,finally 中的代码无论如何都会执行,但是由于当前线程加锁失败并没有持有 lock 对象锁,所以程序会抛出异常。