Java线程系列(3)——线程的动作与状态的关系
前两篇文章分析了Java线程的六种状态以及每种状态的对应的 thread dump 的特性, 调用栈反应了线程执行的代码路径, 调用修饰反应出了线程执行过程中 等待锁/拥有锁/释放锁 的附加信息。线程的状态:
线程dump信息的第一行, 会有waiting on condition
、waiting for monitor entry
、in Object.wait()
等信息, 它其实是线程的动作(即线程的行为)。线程的动作造成线程状态的改变, 也就是说线程的动作是线程状态产生的原因。
本文是Java线程系列文章的第三篇, 主要来讨论线程的动作, 以及具体的动作产生的线程状态。
1. 线程的动作
线程的动作和线程的状态并不是一一对应的关系, 即两个线程执行了同样的动作, 但是可以产生不同的状态, 这之间的差别主要由线程调用Java层面的API决定。线程的动作有如下几种:
- runnable
- in Object.wait()
- waiting for monitor entry
- waiting on condition
2. runnable
代表当前的线程正在执行, 或者它拿到了CPU的执行时间, 正准备执行。一般这种情况下线程的状态为RUNNABLE
。但要注意, 是不是线程的状态为RUNNABLE
时, 它一定在执行呢? 不一定, 因为Java线程的状态代表了线程在Java虚拟机中的执行状态, 并不能真实反映它在操作系统中的状态。RUNNABLE
的官方解释为: A thread in the runnable state is executing in the Java virtual machine but it may be waiting for other resources from the operating system such as processor.
3. waiting for monitor entry & in Object.wait()
waiting for monitor entry
和 in Object.wait()
这两个动作和线程的同步与协作有关。在多线程的Java程序中,实现线程之间的同步协作, 其中一种非常常用的方案是Monitor。Monitor在
Java线程系列(1)——thread dump格式、锁与线程的状态 — Java Monitor
讨论过, 这里不再重复。
3.1 waiting for monitor entry
线程的动作为waiting for monitor entry
, 代表当前线程正在对象锁的 Entry Set 区中竞争锁。如果竞争锁超时, 则线程的状态为BLOCKED (on object monitor)
, on object monitor 修饰了线程阻塞的原因, 同时印证了线程正在竞争锁。如果竞争锁成功, 则调用修饰中会有- locked
修饰。
3.2 in Object.wait()
线程的动作为in Object.wait()
, 代表线程释放锁进入锁的 Wait Set 区中等待特定的动作(其他线程进行唤醒或者超时等)。这时线程的状态是什么, 则由线程调用的 Object.wait
方法决定:
当调用 Object.wait()
时, 线程的状态为 WAITING
, 即线程无期限等待其他线程的动作。例子如下:
package liyin.code;
public class App {
public static void main(String[] args) {
Runnable task = new Runnable() {
public void run() {
// App 类的字节码对象锁
synchronized (App.class) {
try {
// 进入 App 类的字节码对象锁的 Wait Set
App.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
new Thread(task, "bh_001").start();
}
}
"bh_001" prio=5 tid=0x00007fb6f687a800 nid=0x5403 in Object.wait() [0x00007000014d2000]
java.lang.Thread.State: WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000007aab24250> (a java.lang.Class for liyin.code.App)
at java.lang.Object.wait(Object.java:503)
at liyin.code.App$1.run(App.java:13)
- locked <0x00000007aab24250> (a java.lang.Class for liyin.code.App)
at java.lang.Thread.run(Thread.java:745)
当调用 Object.wait(timeout)
时, 线程的状态为 TIMED_WAITING
, 即线程有期限等待其他线程的动作。如果等待时间超过timeout, 则线程自行唤醒并参与到锁竞争中。例子如下:
package liyin.code;
public class App {
public static void main(String[] args) {
Runnable task = new Runnable() {
public void run() {
// App 类的字节码对象锁
synchronized (App.class) {
try {
// 进入 App 类的字节码对象锁的 Wait Set
App.class.wait(20000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
new Thread(task, "bh_002").start();
}
}
"bh_002" prio=5 tid=0x00007fd35a85b800 nid=0x5503 in Object.wait() [0x0000700001555000]
java.lang.Thread.State: TIMED_WAITING (on object monitor)
at java.lang.Object.wait(Native Method)
- waiting on <0x00000007aab244a0> (a java.lang.Class for liyin.code.App)
at liyin.code.App$1.run(App.java:13)
- locked <0x00000007aab244a0> (a java.lang.Class for liyin.code.App)
at java.lang.Thread.run(Thread.java:745)
4. waiting on condition
线程的动作是waiting on condition
, 代表线程在等待某个条件的发生或者sleep。此时线程的状态一般是 TIMED_WAITING (parking)
、TIMED_WAITING (sleeping)
, 即有期限的挂起和睡眠状态。具体的例子请参看
Java线程系列(2)——线程有限等待状态分析 — Thread.sleep 和
Java线程系列(2)——线程有限等待状态分析 — JUC限时API。
当线程为 waiting on condition 时,具体是什么原因,可以结合 stacktrace来分析。最常见的情况是线程在等待网络的读写,比如当网络数据没有准备好读时,线程处于这种等待状态,而一旦有数据准备好读之后,线程会重新激活,读取并处理数据。在Java引入 New IO之前,对于每个网络连接,都有一个对应的线程来处理网络的读写操作,即使没有可读写的数据,线程仍然阻塞在读写操作上,这样有可能造成资源浪费,而且给操作系统的线程调度也带来压力。在 New IO里采用了新的机制,编写的服务器程序的性能和可扩展性都得到提高。
如果发现有大量的线程都处在 Wait on condition,从线程 stack看, 正等待网络读写,这可能是一个网络瓶颈的征兆。因为网络阻塞导致线程无法执行。一种情况是网络非常忙,几乎消耗了所有的带宽,仍然有大量数据等待网络读写;另一种情况也可能是网络空闲,但由于路由等问题,导致包无法正常的到达。所以要结合系统的一些性能观察工具来综合分析,比如 netstat统计单位时间的发送包的数目,看是否很明显超过了所在网络带宽的限制;观察cpu的利用率,看系统态的CPU时间是否明显大于用户态的CPU时间;如果程序运行在 Solaris 10平台上,可以用dtrace工具看系统调用的情况,如果观察到 read/write的系统调用的次数或者运行时间遥遥领先;这些都指向由于网络带宽所限导致的网络瓶颈。