JVM调试常用命令——jstack命令与线程状态(4)
(接上文《JVM调试常用命令——jstack命令与线程状态(3)》)
2.1.4、从Runnable状态进入TIMED_WATING状态
处于Runnable状态的线程,可以使用多种方法使其进入TIMED_WATING状态,这些TIMED_WATING状态还有一些细微的差别,这里我们分别进行详细介绍。
2.1.4.1、当前运行线程调用sleep方法
调用sleep方法使线程进入TIMED_WATING状态是最常见的一种方式,也是笔者不推荐在正式环境中使用的方式。先上测试代码:
// …………之前的代码省略
public static void main(String[] args) {
// 这里的测试代码很简单,
// 就是启动一个名叫"test thread"的线程,并调用sleep方法
new Thread(() -> {
try {
Thread.sleep(100000l);
} catch (InterruptedException e) {
e.printStackTrace(System.out);
}
} , "test thread").start();
}
// …………之后的代码省略
以上的代码调用sleep方法,使其进入TIMED_WATING状态。通过jstack命令我们可以观察阻塞效果,如下所示:
# jstack 9228
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.144-b01 mixed mode):
"test thread" #14 prio=5 os_prio=0 tid=0x000000001f3b0800 nid=0x13f0 waiting on condition [0x000000001fc5f000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at testThread.TestSleep.lambda$0(TestSleep.java:8)
at testThread.TestSleep$$Lambda$1/519569038.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
请注意这是线程“test thread”的线程栈信息,进入了TIMED_WATING状态后标识为“sleeping”。并且从打印出来的线程栈可以看到,这个处于TIMED_WATING状态的线程也并没有获得任何对象的“锁”。
2.1.4.2、当前运行线程调用wait(time)方法
本小节我们调用wait(time)方法,让指定线程进入阻塞状态,并使用jstatck命令观察、验证阻塞效果。首先还是上代码:
// …………之前的代码省略
public static void main(String[] args) {
new Thread(() -> {
try {
synchronized (TestWaitTime.class) {
TestWaitTime.class.wait(100000l);
}
} catch (InterruptedException e) {
e.printStackTrace(System.out);
}
} , "wait thread").start();
}
// …………之后的代码省略
如上代码所示,并没有一个线程进行所谓的“notify”或者“notifyAll”操作,只是启动了一个名叫“wait thread”的线程,并调用wait(time)方法进入阻塞状态。通过jstatck命令我们可以观察到详细的阻塞状态,如下所示:
# jstack 3500
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.144-b01 mixed mode):
"wait thread" #14 prio=5 os_prio=0 tid=0x000000001fe69800 nid=0xeb8 in Object.wait() [0x000000002074f000]
java.lang.Thread.State: TIMED_WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x000000076c5c47f0> (a java.lang.Class for testThread.TestWaitTime)
at testThread.TestWaitTime.lambda$0(TestWaitTime.java:12)
- locked <0x000000076c5c47f0> (a java.lang.Class for testThread.TestWaitTime)
at testThread.TestWaitTime$$Lambda$1/519569038.run(Unknown Source)
at java.lang.Thread.run(Thread.java:748)
如上所示,调用wait(time)方法后,线程如预想一样进入了阻塞状态,提示状态为TIMED_WAITING,但是标识信息就不再提示“sleeping”了,而是“on object monitor”。
2.1.4.3、运行线程调用LockSupport.parkUntil(deadline)、LockSupport.parkNanos(deadline)方法(或基于AQS原理的类似方法)
LockSupport.parkNanos方法的作用,是让指定的线程等待指定的“纳秒”时间,如果期间没有调用者使用unpark方法进行唤醒,当到了时间以后,该线程就会自动解除阻塞状态。parkUntil方法和parkNanos方法不同的是,其给定的参数代表未来的某个时间点(单位毫秒),例如以下语句标识从当前执行时间点开始的5秒以后,解除“阻塞”状态:
LockSupport.parkUntil(new Date().getTime() + 5000l);
parkNanos、parkUntil和unpark方法的组合,工作效果类似于wait方法和notify、notifyAll方法的组合,但其内部使用的锁机制完全不一样(一个是AQS的锁,另一个是基于object monitor的锁)。我们先来看实际的编码示例:
// ......之前的代码片段可以忽略
public static void main(String[] args) {
Thread thread2 = new Thread(new MyThread2(), "thread2");
Thread thread1 = new Thread(new MyThread1(thread2), "thread1");
thread1.start();
thread2.start();
}
private static class MyThread2 implements Runnable {
@Override
public void run() {
// 单位纳秒,以下设置为100秒
LockSupport.parkNanos(100000000000l);
System.out.println("// do somethings");
}
}
private static class MyThread1 implements Runnable {
private Thread targetThread;
public MyThread1(Thread targetThread) {
this.targetThread = targetThread;
}
@Override
public void run() {
// 试图进行唤醒
LockSupport.unpark(targetThread);
System.out.println("// do somethings");
}
}
// ......之后的代码片段可以忽略
纳秒级别的单位基本上已经和Hz(赫兹)这样的单位平级了,是非常微度化的单位——1毫秒=1000000纳秒。Java作为高级语言是无法精确控制1纳秒和2纳秒的时间差异的,所以只能将调用硬件级别的API来进行控制——具体请阅读sun.misc.Unsafe中的源代码。
通过编程工具提供的debug控制,我们可以将两个线程的执行情况控制在,thread2已经调用了parkNanos方法进入阻塞状态,而thread1还没有调用unpark方法的时间点。这时我们使用jstack命令,观察整个Java进程的工作情况,如下所示:
# jstack 58060
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.144-b01 mixed mode):
"thread2" #14 prio=5 os_prio=0 tid=0x000000001e577800 nid=0x13eec waiting on condition [0x000000001f4ef000]
java.lang.Thread.State: TIMED_WAITING (parking)
at sun.misc.Unsafe.park(Native Method)
at java.util.concurrent.locks.LockSupport.parkNanos(LockSupport.java:338)
at testThread.TestLockSupport$MyThread2.run(TestLockSupport.java:15)
at java.lang.Thread.run(Thread.java:748)
阅读过之前文章的读者实际上这个时候就应该可以猜测到jstack命令的输出结果了:名叫thread2的线程进入了TIMED_WAITING状态,且标识信息为“parking”。这时同样可以观察到,这个名叫thread2的线程并没有获得任何对象的锁。(如果使用类似parkNanos(object, nanos)这样的方法,就可以在线程栈中观察到“parking to wait for XXXXXX object”这样的提示信息,但同样不是locked XXXX object的提示)
2.1.5、要点说明
-
AQS原理在Java中的使用非常广泛
基于AQS的锁机制在Java中的应用非常广泛,而LockSupport工具类作为这种机制的基础支撑工具,就具有更广泛的使用。例如线程池ThreadPoolExecutor中最基本的任务控制单位java.util.concurrent.ThreadPoolExecutor.Worker,是基于AQS原理进行实现;我们经常使用的线程信号控制工具java.util.concurrent.CountDownLatch、java.util.concurrent.Semaphore是基于AQS进行实现的;本专题之前讨论的可重入锁(java.util.concurrent.locks.ReentrantLock)和读写分离的可重入锁(java.util.concurrent.locks.ReentrantReadWriteLock)的具体实现原理也是基于AQS的。后文我们会详细介绍AQS锁机制,并讨论它和object monitor锁机制的区别。
-
我们通常使用的部分CAS性质的原子操作是无法监控锁效果的,因为没有必要。
在本专题之前的文章中,我们详细介绍了悲观锁原理和乐观锁原理。这里要说明的是,基于乐观锁原理实现进行的部分实现,是无法通过jstack命令观察到其“阻塞”状态的。例如“java.util.concurrent.atomic”包中的原子操作类就是一组典型的基于乐观锁原理实现的原子性操作工具类,当多个线程使用java.util.concurrent.atomic.AtomicInteger进行原子性操作时,这些线程都不会观察到其在jstack命令输出结果中的阻塞状态。
但并不是所有的基于乐观锁实现的工具,都看不到锁状态——例如我们随后详细介绍的AQS框架及其实现,从本质上来讲就是一种乐观锁的实现思路,但是很明显,我们可以通过jstack命令观察到其锁状态。 -
底层的sun.misc.Unsafe类非常重要
无论是AQS锁机制在Java中的实现,还是在本专题中已经介绍的乐观锁实现,其最底层都是基于sun.misc.Unsafe类这个类。这个类在JDK1.7、JDK1.8以及之后的版本中(一直到JDK11)都得到了不同的加强。建议读者花时间详细理解其中的代码。