Java线程调度详解
- 创建状态:实例化了一个新的线程对象,还未启动。
- 就绪状态:创建好的线程对象调用start()方法完成启动,进入线程池等待抢占CPU资源。
- 运行状态:线程对象获取了CPU资源,在一定的时间内执行任务。
- 阻塞状态:正在运行的线程暂停执行任务,释放所占用的CPU资源。并在解除阻塞之后也不能直接回到运行状态,而是重新回到就绪状态,等待获取CPU资源。
- 终止状态:线程运行完毕或因为异常导致该线程终止运行。
线程状态之间的转换如下图所示:
图 1 线程状态
线程调度
1) 线程休眠
休眠指让当前线程暂停执行,从运行状态进入阻塞状态,将 CPU 资源让给其他线程的一种调度方式,要通过调用 sleep() 方法来实现。sleep(long millis) 是 java.lang.Thread 类中定义的方法,使用时需要指定当前线程休眠的时间,传入一个 long 类型的数据作为休眠时间,单位为毫秒。任意一个线程的实例化对象都可以调用该方法。
例如:
public class MyThread extends Thread{ @Override public void run() { // TODO Auto-generated method stub for (int i = 0; i < 10; i++) { if(i == 5) { try { sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } System.out.println("--------------MyThread"); } } }当循环执行到第 6 次即 i=5 时,会休眠 1000 毫秒,1000 毫秒之后线程进入就绪状态,重新等待系统为其分配 CPU 资源。这是在线程内部执行休眠操作,也可以在外部使用线程时执行休眠操作,比如说:
public class MyThread extends Thread{ @Override public void run() { // TODO Auto-generated method stub for (int i = 0; i < 10; i++) { System.out.println("--------------MyThread"); } } } public class Test { public static void main(String[] args) { MyThread myThread = new MyThread(); try { myThread.sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } mt.start(); for(int i = 0; i < 100; i++) { System.out.println("++++++++++++++Test"); } } }在 Test 类的主线程中创建 MyThread 子线程,程序运行时 MyThread 子线程休眠 1000 毫秒之后再启动。
前面我们说到任意一个线程的实例化对象都可以调用 sleep() 方法,即每一个线程都可以进行休眠。那么如何让主线程休眠呢?
主线程并不是一个我们手动实例化的线程对象,不能直接调用 sleep() 方法,这种情况下可以通过 Threa 类的静态方法 currentThread 来获取主线程对应的线程对象,然后调用 sleep() 方法,例如:
public class Test { public static void main(String[] args) { for(int i = 0; i < 10; i++) { if(i == 5) { try { Thread.currentThread().sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } System.out.println("++++++++++++++Test"); } } }主线程循环执行到第 6 次即 i=5 时,会进行休眠状态,然后暂停执行,等待 1000 毫秒之后进入就绪状态,并等待获取 CPU 资源从而进入运行状态继续执行。
无论通过哪种方式调用 sleep() 方法,都需要注意处理异常,因为 sleep() 方法在定义时声明了可能会抛出的异常 InterruptedException,例如:
public static native void sleep(long millis) throws InterruptedException;所以在外部调用 sleep() 方法时就必须处理可能抛出的异常,这里给出两种方案:
- 通过 try-catch 主动捕获;
- main 方法定义处抛出该异常交给 JVM 去处理。
推荐使用第 1 种方案。
2) 线程合并
合并的意思是将指定的某个线程加入到当前线程中,合并为一个线程,由两个线程交替执行变成一个线程中的两个子线程顺序执行,即一个线程执行完毕之后再来执行第二个线程。可以通过调用线程对象的 join() 方法来实现合并。具体是如何来合并的呢,谁为主谁为从?
假设有两个线程,分别是线程甲和线程乙。线程甲在执行到某个时间点的时候调用线程乙的 join() 方法,则表示从当前时间点开始 CPU 资源被线程乙独占,线程甲进入阻塞状态。直到线程乙执行完毕,线程甲进入就绪状态,等待获取 CPU 资源进入运行状态继续执行,代码如下所示:
public class JoinRunnable implements Runnable{ @Override public void run() { // TODO Auto-generated method stub for(int i = 0 ; i < 20; i++) { System.out.println(i+"----------JoinRunnable"); } } } public class Test { public static void main(String[] args) { JoinRunnable joinRunnable = new JoinRunnable(); Thread thread = new Thread(joinRunnable); thread.start(); for(int i = 0; i < 100; i++) { if(i == 10) { try { thread.join(); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } System.out.println(i+"++++++++++main"); } } }通过实现接口的方式定义 JoinRunnable,在 Test 类的主线程中开启子线程。当主线程循环执行到 i==10 的节点时,将子线程合并到主线程中,此时主线程进入阻塞状态。而后子线程执行,当子线程执行完毕之后,主线程继续执行,运行结果为:
......
10----------JoinRunnable
11----------JoinRunnable
12----------JoinRunnable
13----------JoinRunnable
14----------JoinRunnable
15----------JoinRunnable
16----------JoinRunnable
17----------JoinRunnable
18----------JoinRunnable
19----------JoinRunnable
10++++++++++main
11++++++++++main
12++++++++++main
......
同样是完成线程合并的操作,join() 和 join(long millis) 还是有区别的,join() 表示在被调用线程执行完成之后才能执行其他线程。join(long millis) 则表示被调用线程执行 millis 毫秒之后,无论是否执行完毕,其他线程都可以和它来争夺 CPU 资源。
join(long millis) 的具体使用可以参考下面的代码:
public class JoinRunnable implements Runnable{ @Override public void run() { // TODO Auto-generated method stub for(int i = 0 ; i < 20; i++) { try { Thread.currentThread().sleep(1000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } System.out.println(i+"----------JoinRunnable"); } } } public class Test { public static void main(String[] args) { JoinRunnable joinRunnable = new JoinRunnable(); Thread thread = new Thread(joinRunnable); thread.start(); for(int i = 0; i < 100; i++) { if(i == 10) { try { thread.join(3000); } catch (InterruptedException e) { // TODO Auto-generated catch block e.printStackTrace(); } } System.out.println(i+"++++++++++main"); } } }运行结果为:
......
4++++++++++main
5++++++++++main
6++++++++++main
7++++++++++main
8++++++++++main
9++++++++++main
0----------JoinRunnable
1----------JoinRunnable
......
3) 线程礼让
线程礼让是指在某个特定的时间点,让线程暂停抢占 CPU 资源的行为,即从运行状态或就绪状态来到阻塞状态,从而将 CPU 资源让给其他线程来使用。现实生活中地铁排队进站,排到该你进站的时候,你让其他人先进,把这次进站的机会让给其他人。但是这并不意味着你放弃排队,你只是在某个时间点做了一次礼让,过了这个时间点,你依然要参与到排队的序列中。
线程中的礼让也是如此,假如有线程甲和线程乙在交替执行,在某个时间点线程甲做出了礼让,所以在这个时间节点线程乙拥有了 CPU 资源,执行其业务逻辑,但不是说线程甲会一直暂停执行,直到线程乙执行完毕再来执行线程甲。线程甲只是在特定的时间节点礼让,过了这个时间节点,线程甲再次进入就绪状态,和线程乙争夺 CPU 资源。
Java 中的线程礼让,通过调用 yield() 方法完成,具体实现如下:
public class YieldThread1 extends Thread{ @Override public void run() { // TODO Auto-generated method stub for (int i = 0; i < 10; i++) { if(i == 5) { Thread.currentThread().yield(); } System.out.println(Thread.currentThread().getName()+"------"+i); } } } public class YieldThread2 extends Thread{ @Override public void run() { // TODO Auto-generated method stub for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName()+"------"+i); } } } public class Test { public static void main(String[] args) { YieldThread1 thread1 = new YieldThread1(); thread1.setName("Thread-1"); YieldThread2 thread2 = new YieldThread2(); thread2.setName("Thread-2"); thread1.start(); thread2.start(); } }定义两个线程类 YieldThread1 和 YieldThread2,循环执行输出语句,并且 YieldThread1 中的循环执行到 i=5 时,会做出礼让,将 CPU 资源让给 YieldThread2。通过调用 setName() 方法可以给线程对象自定义名称,在测试类中创建两个线程对象并启动。
仔细观察运行结果可以发现,前半段的 YieldThread1 和 YieldThread2 是在交替执行。当 YieldThread1 的 i=5 时,它暂停了执行,YieldThread2 开始执行,但是从下一个时刻起,YieldThread1 又参与到 CPU 资源的争夺中。
4) 线程中断
有多种情况可以造成线程停止运行,例如:- 线程执行完毕之后会自动停止该线程;
- 线程执行过程中遇到错误会抛出异常并停止该线程;
- 线程在执行过程中会根据需求手动停止该线程。
我们要介绍的线程中断就是第 3 种情况,线程在执行过程中,通过手动操作来停止该线程。
比如当用户在执行一个操作时,因为网络问题导致延迟,则对应的线程对象就一直处于运行状态,如果用户希望结束这个操作,即终止该线程,此时我们就要使用线程中断机制了。
Java 中实现线程中断机制有如下几个常用方法:
- public void stop()
- public void interrupt()
- public boolean isInterrupted()
其中 stop 方法在新版本的 JDK 中已经不推荐使用了,所以我们这里不再对 stop 方法进行讲解,重点关注其他两个方法。
interrupt 是一个实例方法,当一个线程对象调用该方法时,表示中断当前线程对象。每个线程对象都是通过一个标志位来判断当前是否为中断状态,isInterrupted() 方法就是用来获取当前线程对象的标志位的。true 表示清除了标志位,当前线程对象已经中断;false 表示没有清除标志位,当前对象没有中断。当一个线程对象处于不同的状态时,中断机制也是不同的,接下来我们分别演示不同状态下的线程中断。
创建状态:实例化线程对象,但并未启动,代码如下:
public class Test { public static void main(String[] args) { Thread thread = new Thread(); //获取当前线程对象的状态 System.out.println(thread.getState()); thread.interrupt(); System.out.println(thread.isInterrupted()); } }
getState() 方法可以获取当前线程对象的状态,实例化一个线程对象 thread,但是并未启动该对象,直接中断,运行结果为:
NEW
false
运行状态:实例化线程对象,启动该线程,循环输出语句,当 i=5 时中断线程,代码如下:
public class Test { public static void main(String[] args) { Thread thread = new Thread(new Runnable() { @Override public void run() { // TODO Auto-generated method stub for (int i = 0; i < 10; i++) { if(i == 5) { Thread.currentThread().interrupt(); } System.out.println("++++++++++Test"); } } }); thread.start(); System.out.println(thread.getState()); System.out.println(thread.isInterrupted()); System.out.println(thread.getState()); } }运行结果为:
++++++++++Test
++++++++++Test
++++++++++Test
++++++++++Test
++++++++++Test
RUNNABLE
++++++++++Test
++++++++++Test
++++++++++Test
++++++++++Test
++++++++++Test
true
TERMINATED