1. 线程的调度与时间片
1.1 java线程与操作系统
- 现代操作系统(如Windows、Linux、Solaris)提供了强大的线程管理能力,Java不需要再进行自己独立的线程管理和调度,而是将线程调度工作委托给操作系统的调度进程去完成。在某些系统(比如Solaris操作系统)上,JVM甚至将每个Java线程一对一地对应到操作系统的本地线程,彻底将线程调度委托给操作系统。
1.2 CPU时间片
-
由于CPU的计算频率非常高,每秒计算数十亿次,因此可以将CPU的时间从毫秒的维度进行分段,每一小段叫作一个CPU时间片。
-
对于不同的操作系统、不同的CPU,线程的CPU时间片长度都不同。假定操作系统(比如Windows XP)线程的时间片长度为20毫秒,在一个2GHz的CPU上,一个时间片可以进行计算的次数是
20亿/(1000/20)=4000万次
,也就是说,一个时间片内的计算量是非常巨大的。 -
目前操作系统中主流的线程调度方式是: 基于CPU时间片方式进行线程调度。
-
线程只有得到CPU时间片才能执行指令,处于执行状态,没有得到时间片的线程处于就绪状态,等待系统分配下一个CPU时间片。
-
由于时间片非常短,在各个线程之间快速地切换,因此表现出来的特征是很多个线程在
同时执行
或者并发执行
-
线程的调度模型目前主要分为两种:分时调度模型和抢占式调度模型。
分时调度模型
: 系统平均分配CPU的时间片,所有线程轮流占用CPU。分时调度模型在时间片调度的分配上,所有线程人人平等
三个线程轮流得到CPU时间片,一个线程执行时,另外两个线程处于就绪状态。
抢占式调度模型
:系统按照线程优先级分配CPU时间片。优先级高的线程,优先分配CPU时间片,如果所有就绪线程的优先级相同,那么会随机选择一个,优先级高的线程获取的CPU时间片相对多一些。
由于目前大部分操作系统都是使用抢占式调度模型进行线程调度,Java的线程管理和调度是委托给操作系统完成的,所以,Java的线程调度也是使用抢占式调度模型,因此Java的线程都有优先级。
2. 线程的优先级
-
在Thread类中有一个实例属性和两个实例方法,专门用于进行线程优先级相关的操作,与线程优先级相关的成员属性为:
private int priority; //该属性保存一个Thread实例的优先级,即1~10之间的值
-
与Thread类线程优先级相关的实例方法为:
public final int getPriority() // 获取线程优先级。 public final void setPriority(int priority) // 设置线程优先级。
Thread实例的priority属性默认是级别5,对应的类常量是NORM_PRIORITY。
优先级最大值为10,最小值为1,Thread类中定义的三个优先级常量如下:
public static final int MIN_PRIORITY=1; public static final int NORM_PRIORITY=5; public static final int MAX_PRIORITY=10;
Java中使用抢占式调度模型进行线程调度。priority实例属性的优先级越高,线程获得CPU时间片的机会越多,但也不是绝对的。
public class PriorityDemo { public static final int SLEEP_GAP = 1000; static class PrioritySetThread extends Thread { static int threadNo = 1; public PrioritySetThread() { super("thread-" + threadNo); threadNo++; } public long opportunities = 0; public void run() { for (int i = 0; ; i++) { opportunities++; } } } public static void main(String args[]) throws InterruptedException { PrioritySetThread[] threads = new PrioritySetThread[10]; for (int i = 0; i < threads.length; i++) { threads[i] = new PrioritySetThread(); //优先级的设置,从1-10 threads[i].setPriority(i + 1); } for (int i = 0; i < threads.length; i++) { threads[i].start(); } Thread.sleep(SLEEP_GAP); for (int i = 0; i < threads.length; i++) { threads[i].stop(); } for (int i = 0; i < threads.length; i++) { System.out.println(threads[i].getName() + ";优先级为-" + threads[i].getPriority() + ";机会值为-" + threads[i].opportunities ); } } }
10个线程中某个线程的实例属性opportunities的值越大,就表明该线程获得的CPU时间片越多。
- 高优先级的线程获得的执行机会更多。可以看到: 优先级在6级以上的线程执行机会明显偏多,整体对比非常明显。
- 执行机会的获取具有随机性,优先级高的不一定获得的机会多。比如,例子中的thread-10比thread-9优先级高,但是thread-10所获得的机会反而偏少。
3. 线程声明周期
-
Java中的线程的生命周期分为6种状态。
-
Thread类有一个实例属性和一个实例方法专门用于保存和获取线程的状态。
- 用于保存线程Thread实例状态的实例属性为:
private int threadStatus; // 以整数的形式保存线程的状态。
- Thread类用于获取线程状态的实例方法为:
public Thread.state getstate(); // 返回当前线程的执行状态,一个枚举类型值
Thread.State是一个内部枚举类,定义了6个枚举常量,分别代表Java线程的6种状态,具体如下:
public enum State { NEW, // 新建 RUNNABLE,// 就绪、运行 BLOCKED,// 阻塞 WAITING, // 等待 TIMED_WAITING, // 计时等待 TERMINATED; // 结束 }
在Thread.State 定义的6种状态中,有4种是比较常见的状态
- NEW(新建)状态
- RUNNABLE(可执行)状态
- TERMINATED(终止)状态
- TIMED_WAITING(限时等待)状态。
3.1 NEW 状态
- Java源码对NEW状态的注释说明是: 创建成功但是没有调用start()方法启动的Thread线程实例都处于NEW状态。
- Thread线程实例的调用start()方法之后,其状态就从NEW状态到RUNNABLE状态,但并不意味着线程立即就能获取CPU时间片并且立即执行,中间需要一系列的操作系统内部操作。
3.2 RUNNABLE 状态
- 当调用了Thread实例start()方法后,下一步如果线程获取CPU时间片开始执行,JVM将异步调用线程的run()方法执行其业务代码。
- 在run()方法被异步调用之前,JVM做了哪些事情呢?
- JVM的幕后工作和操作系统的线程调度有关。Java中的线程管理是通过JNI本地调用的方式,委托操作系统的线程管理API完成的。
- 当Java线程的Thread实例的start()方法被调用后,操作系统中的对应线程进入的并不是运行状态,而是就绪状态,而Java线程并没有这个就绪状态。操作系统中线程的就绪状态是什么状态的呢?JVM的线程状态与其幕后的操作系统线程状态之间的转换关系如图:
-
一个操作系统线程如果处于就绪状态,表示该线程已经满足了执行条件,但是还不能执行。处于就绪状态的线程需要等待操作系统的调度,一旦就绪状态线程被操作系统选中,获得CPU时间片,线程就开始占用CPU,开始执行线程的代码,这时线程的操作系统状态发生了改变,进入了运行状态。
-
在操作系统中,处于运行状态的线程在CPU时间片用完之后,又回到就绪状态,等待CPU的下一次调度。就这样,操作系统线程在就绪状态和执行状态之间被系统反复地调度,一直持续直到线程的代码逻辑执行完成或者异常终止。这时线程的操作系统状态又发生了改变,进入了线程的最后状态
TERMINATED
状态。 -
就绪状态和运行状态都是操作系统中的线程状态。在Java语言中,并没有细分这两种状态,而是将这两种状态合并成同一种状态RUNNABLE状态。因此,在Thread.State枚举类中,没有定义线程的就绪状态和运行状态,只是定义了RUNNABLE状态。这就是Java线程状态和操作系统中的线程状态有所不同的地方。
NEW状态的Thread实例调用了start()方法后,线程的状态将变成RUNNABLE状态。
尽管如此,线程的run()方法不一定会马上被并发执行,需要在线程获取了CPU时间片之后,才会真正启动并发执行。
3.3 TERMINATED 状态
- 处于RUNNABLE状态的线程在run()方法执行完成之后就变成终止状态TERMINATED了。
- 如果在run()方法执行过程中发生了运行时异常而没有被捕获,run()方法将被异常终止,线程也会变成TERMINATED状态。
3.4 TIMED_WAITING 限时等待状态
- 线程处于一种特殊的等待状态,准确地说,线程处于限时等待状态。能让线程处于限时等待状态的操作大致有以下几种:
Thread.sleep(int n) // 使得当前线程进入限时等待状态,等待时间为n毫秒。
Object.wait() // 带时限的抢占对象的monitor锁。
Thread.join() // 带时限的线程合并。
LockSupport.parkNanos() // 让线程等待,时间以纳秒为单位。
LockSupport.parkUntil() // 让线程等待,时间可以灵活设置。
4. 使用 Jstack 工具查看线程状态
-
有时,服务器CPU占用率会一直很高,甚至一直处于100%。如果CPU使用率居高不下,自然是有某些线程一直占用着CPU资源,如何査看CPU占用率较高的线程呢?或者说,如何查看到线程的状态呢?一种比较快捷的办法是使用Jstack工具。
-
Jstack工具是Java虚拟机自带的一种堆栈跟踪工具。Jstack用于生成或导出(DUMP)IVM虚拟机运行实例当前时刻的线程快照。线程快照是对当前JVM实例内每一个线程正在执行的方法堆栈的集合,生成或导出线程快照的主要目的是用于定位线程出现长时间运行、停顿或者阻塞的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待等。线程出现停顿的时候通过Jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。
Jstack命令的语法格式:jstack <pid> // pid表示Java进程id,可以用jps命令查看
-
一般情况下,通过Jstack输出的线程信息主要包括: JVM线程、用户线程等。
- JVM线程会在JVM启动时就存在,主要用于执行譬如垃圾回收、低内存的检测等后台任务,这些线程往往在JVM初始化的时候就存在。
- 用户线程则是在程序创建了新的线程时才会生成。这里要注意的是:
- 在实际运行中,往往一次DUMP的信息不足以确认问题。建议产生三次DUMP信息,如果每次DUMP都指向同一个问题,我们才确定问题的典型性。
- 不同的Java虚拟机的线程导出来的DUMP信息格式是不一样的,并且同一JVM的不同版本DUMP信息也有差别。
- GC task thread为垃圾回收线程,此类该线程会负责进行垃圾回收。通常JVM会启动多个GC线程,在GC线程的名称中,#后面的数字会累加,如GC task thread#1、GC task thread#2等。
- VM Periodic Task Thread线程是JVM周期性任务调度的线程,该线程在JVM内使用得比较频繁,比如定期的内存监控、JVM运行状况监控。
- Jstack指令所输出的信息中包含以下重要信息:
- tid: 线程实例在JVM进程中的id。
- nid: 线程实例在操作系统中对应的底层线程的线程id。
- prio:线程实例在JVM进程中的优先级。
- os prio:线程实例在操作系统中对应的底层线程的优先级。
- 线程状态:如runnable、waiting on condition等。