分析运行时Java应用性能问题或死锁问题时,获取线程栈中锁信息是最基本的手段。JDK5.0之后加锁存在两种方法:1.Jvm内置的synchronized 2. Java并发包中相关同步类。本篇主要讨论线程栈中表现出来的锁的信息,对于我们分析Java性能或死锁提供哪些线索。
情况1: Object.wait()/ Object.wait(int timeout)
1 | public class InvokeWait { |
1 | "Test-Thread-1" #10 prio=5os_prio=0 tid=0x0000000058709800 nid=0x1d14 in Object.wait() [0x000000005930f000] |
这种情况下,线程处于WAITING状态,等待获取对象锁,该线程必须先锁定了该对象(即进入synchronized区域),才可以调用wait方法(画重点),并在调用wait方法后释放对象锁,并进入对象锁Wait Set队列等待被唤醒。WAITING状态的线程必须有对应的notify唤醒。这与后面遇到Blocked状态的线程不同,Blocked是不需要唤醒的。
备注:
1) 每一个Java对象与生俱来带有唯一一把对象锁(叫Intrinsic lock或Monitor)。
2) (a java.lang.Object)表示资源的类名,这里为一个Object对象。
3) 当调用带有时间参数的Wait方法,这时线程状态显示TIMED_WAITING。
4)Wait Set队列与下面的Entry Set队列的含义,可自行Google。
情况2: 资源争用
1 | public class Share { |
1 | "Test-thrad-2" #11 prio=5os_prio=0 tid=0x0000000058a09000 nid=0x1c94 waiting for monitor entry [0x00000000595af000] |
Test-thrad-2处于BLOCKED状态,说明线程Test-thrad-2进入synchronized临界区, 进入对象锁的Entry Set队列并等待对象锁0x00000000d79d8d38的释放。通过定位locked <0x00000000d79d8d38>,我们很容易发现哪个线程正在占用这把锁。在性能分析过程,如果发现大量的BLOCKED线程,我们就需要分析谁当前持有这把锁,而且长时间没有释放。 通过locked指示的代码行,我们很容易定位到加锁位置。然后进行降低锁粒度等手段,减少线程加锁的持续时间,从而优化性能。0x00000000d79d8d38>
情况3 死锁
1 | public class Main { |
1 | "Test-thrad-2" #11 prio=5 os_prio=0tid=0x00000000185c7800 nid=0x2b88 waiting for monitor entry [0x00000000196bf000] |
Test-thrad-2线程拿到了0x00000000d7ac5ea0锁,在等待x00000000d7ac5e90锁,而Test-thrad-1刚好相反,发生了死锁的条件, 而且从线程栈很容易定位这些锁是在哪一行代码获取的,且又在哪一行代码阻塞的。JVM线程栈信息的尾端提供了更详细的死锁环的信息:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21Found one Java-level deadlock:
=============================
"Test-thrad-2":
waiting to lock monitor 0x0000000017235028 (object 0x00000000d7ac5e90, a java.lang.Object),
which is held by "Test-thrad-1"
"Test-thrad-1":
waiting to lock monitor 0x00000000172350d8 (object 0x00000000d7ac5ea0, a java.lang.Object),
which is held by "Test-thrad-2"
Java stack information for the threadslisted above:
===================================================
"Test-thrad-2":
at org.guojje.DeadLock.run(DeadLock.java:21)
- waiting to lock <0x00000000d7ac5e90>(a java.lang.Object)
- locked <0x00000000d7ac5ea0>(a java.lang.Object)
at java.lang.Thread.run(Thread.java:745)
"Test-thrad-1":
at org.guojje.DeadLock.run(DeadLock.java:21)
- waiting to lock <0x00000000d7ac5ea0>(a java.lang.Object)
- locked <0x00000000d7ac5e90> (a java.lang.Object)
at java.lang.Thread.run(Thread.java:745)
Found 1 deadlock.
你会发现JDK内置的synchronized加锁方式,你会发现问题分析死锁问题非常容易。这个例子就是因为竞争线程对资源的加锁顺序不同,导致死锁。而保持相同的加锁顺序是解决这类死锁问题关健。
对于内置的synchronized的同步方式,线程栈提供了足够的信息让我们分析与定位问题。但synchronized不够灵活等,使Java并发包更受程序序员青睐,但同时也为分析这类问题代来复杂性。
情况1
同样,我们先看一个ReentrantLock(重入锁的)await方法(与Ojbect.wait区别开来)的调用情况。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 public class Main {
public static void main(String[] args){
Thread thread = new Thread(new RTLock());
thread.setName("Test-thrad-1");
thread.start();
}
}
class RTLock implements Runnable {
private ReentrantLock lock= newReentrantLock();
public Condition scarce= lock.newCondition();
public RTLock() {
}
public void run() {
lock.lock();
try {
scarce.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally{
lock.unlock();
}
}
}
1 | "Test-thrad-1" #10 prio=5os_prio=0 tid=0x00000000189d0800 nid=0x2da8 waiting on condition [0x000000001968e000] |
显示与synchronized有些不同。Synchronized显示为Wait on,这里- parking to wait for。
另外可以发现调用栈再也找不到locked字眼了,虽然你确认该线程一定locked过这把锁(否则无法调用await)。因为这个原因,为定位加锁位置带来难度。
情况2 资源争用
1 | public class Main { |
1 | "Test-thrad-2" #11 prio=5os_prio=0 tid=0x0000000018802000 nid=0x3368 waiting on condition [0x000000001965f000] |
如果是synchronized的方式,Test-thrad-2线程应该是Blocked状态,但这里为WAITING。这也是内置锁与JUL锁的不同。
注: 只要看Blocked状态的线程,代码一定是用synchronized作的同步。
在线程栈中多出了Locked ownable synchronizers条目。这是用jstack-l打印出来。(-l表示打印更详细的栈信息)。Java已经做了努力,弥补线程栈没有locked信息的缺陷。假如没有这个条目,无法确认Test-thrad-2等待的锁被哪一个线程持有。即便如此,还是无法定位加锁的代码位置,目前本人并没有找到更好的方法,只能翻代码了。
Ownable synchronizers这里的ownable的可以理解为,可被霸占的,即抢到就只属于你的。(有的邪恶哦?)
所以Locked ownable synchronizers只会列出排他锁,如ReentrantLock锁和ReentrantReadWriteLock的写锁,而对于ReentrantReadWriteLock读是不会显示的。因为他不算排他锁(ownable)。某些情况下,这一点也为定位问题加在难度。
注:- locked <0x00000000d79d8d38> (a java.io.BufferedInputStream)是BufferedInputStream.read上的锁不要搞混.0x00000000d79d8d38>
情况3 死锁
1 | public class Main { |
1 | "Test-thrad-2" #11 prio=5os_prio=0 tid=0x0000000018871000 nid=0x2588 waiting on condition [0x00000000198ef000] |
借助Locked ownable synchronizers,确认哪些线程位于死锁的环形竞争中,还是比较容易。先到这吧。。