首页 > 编程笔记

Java线程调度详解

线程共有 5 种状态,在特定的情况下,线程可以在不同的状态之间切换,5 种状态如下所示:
线程状态之间的转换如下图所示:


图 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() 方法时就必须处理可能抛出的异常,这里给出两种方案:
  1. 通过 try-catch 主动捕获;
  2. 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),如果某个时间点在线程甲中调用了线程乙的 sleep(1000) 方法,表示从当前这一时刻起,线程乙会独占 CPU 资源,线程甲进入阻塞状态。当线程乙执行了 1000 毫秒之后,线程甲重新进入就绪状态。

同样是完成线程合并的操作,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
......

当主线程执行到 i==10 时,子线程会合并到主线程中,并且是通过调用 join(3000) 方法完成合并的。所以从此刻开始子线程独占 CPU 资源执行 3000 毫秒之后,主线程继续与其抢占资源。因为子线程每次执行都会休眠 1000 毫秒,所以看到的结果是执行了两次子线程之后,主线程再次进入就绪状态来抢占 CPU 资源。

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 中实现线程中断机制有如下几个常用方法:
其中 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

可以看到结果,NEW 表示当前线程对象为创建状态,flase 表示的是当前线程并未中断。因为当前线程状态根本就没有启动,所以就不存在中断,不需要清除标志位,所以 isInterrupted 的返回值为 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

推荐阅读