1. Lock#lock() 的加锁位置问题
最近在做项目的性能优化,需要将原本单线程跑的程序改造成多线程并行以提高性能。然而业务资源池子是定量的,多线程并行势必涉及到共享资源抢占的问题,需要实现线程间的互斥等待 。这种需求采用同步锁是毋庸置疑的,但是在加锁的位置上却有一些细节,例如加锁操作是否可以放在 try 代码块里面呢?
2. Lock#lock() 加锁位置分析
先给出结论,加锁操作推荐放在 try 代码块外部第一行,以下是 JDK 文档 给出的 Lock 使用示例,可以注意到两点:
- 加锁操作
Lock#lock() 方法调用放在 try 代码块外部第一行 - 解锁操作
Lock#unlock() 方法放在 finally 代码块内
第 2 点没什么好说的,解锁操作在 finally 里保证业务代码出现异常时锁依然能被释放,可以避免死锁产生。关于第 1 点则需要一些澄清,下文将解释以下两种情况可能产生的问题:
在 try 内部加锁 在 try 外部非第一行加锁
Lock l = ...;
l.lock();
try {
} finally {
l.unlock();
}
2.1 加锁在 try 内部可能的问题
以下是将加锁操作放在 try 内部的代码示例,仔细考虑就能发现问题点:
- 假设
Lock#lock() 方法抛出运行时异常,如果加锁放在 try 代码块内部,则必然触发 finally 中的解锁方法 Lock#lunock() 的执行 Lock#lunock() 方法会调用 AQS 的 tryRelease() 方法,如果当前 Lock 的实现类为独占锁(例如 ReentrantLock) ,那么其内部实现会检查当前解锁线程是否是持有锁的线程,如果不是则抛出 IllegalMonitorStateException 异常- 当前线程在加锁方法
Lock#lock() 中抛出异常,显然当前线程未持有锁,然而它却在 finally 中进行解锁操作,通常情况下这里都会抛出一个解锁失败的 IllegalMonitorStateException 异常 - 以上过程中总共产生了两个异常,一个是在加锁时,另一个是在解锁时。然而在控制台查看异常堆栈,会发现加锁时异常堆栈丢失了,只剩下解锁时的异常堆栈,不利于问题排查
Exception in thread "main" java.lang.IllegalMonitorStateException
at java.util.concurrent.locks.ReentrantLock$Sync.tryRelease(ReentrantLock.java:151)
at java.util.concurrent.locks.AbstractQueuedSynchronizer.release(AbstractQueuedSynchronizer.java:1261)
at java.util.concurrent.locks.ReentrantLock.unlock(ReentrantLock.java:457)
以上问题的核心点在于,JDK 的独占锁 Lock 实现在解锁时会对操作线程进行校验,未持有锁的线程进行解锁操作将导致异常
public static void main(String[] args) {
Lock lock = new ReentrantLock();
try {
lock.lock();
} finally {
lock.unlock();
}
}
2.2 加锁在 try 外部非第一行可能的问题
以下是加锁操作在 try 代码块外部非第一行的示例,简单解释下代码执行过程中可能产生的问题:
- 当前线程执行
Lock#lock() 方法正常拿到锁,继续往下执行 - 此处抛一个运行时异常模拟代码执行异常的情况,异常被抛出后当前线程执行中断,而此时线程还没有进入 try 代码块,finally 代码块必然不会执行,解锁操作自然更无从谈起
- 当前线程拿到锁后发生异常,终止执行却没有解锁,毫无疑问死锁就这样产生了
public static void main(String[] args) {
Lock lock = new ReentrantLock();
lock.lock();
if (args.length == 0) throw new RuntimeException();
try {
} finally {
lock.unlock();
}
}
|