在多线程编程中,线程个数一般都大于 CPU 个数,而每个 CPU 同一时刻只能被一个线程使用,为了让用户感觉多个线程是在同时执行的,CPU 资源的分配采用了时间片轮转的策略,也就是给每个线程分配一个时间片,线程在时间片内占用 CPU 执行任务。当前线程使用完时间片后,就会处于就绪状态并让出 CPU 让其他线程占用,这就是上下文切换,从当前线程的上下文切换到了其他线程。那么就有一个问题,让出 CPU 的线程等下次轮到自己占有 CPU 时如何知道自己之前运行到哪里了?所以在切换线程上下文时需要保存当前线程的执行现场,当再次执行时根据保存的执行现场信息恢复执行现场。

线程上下文切换时机有:当前线程的 CPU 时间片使用完处于就绪状态时,当前线程被其他线程中断时。

让线程睡眠的 sleep 方法

Thread 类中有一个静态的 sleep 方法,当一个执行中的线程调用了 Thread 的 sleep 方法后,调用线程会暂时让出指定时间的执行权,也就是在这期间不参与 CPU 的调度,但是该线程所拥有的监视器资源,比如锁还是持有不让出的。指定的睡眠时间到了后该函数会正常返回,线程就处于就绪状态,然后参与 CPU 的调度,获取到 CPU 资源后就可以继续运行了。如果在睡眠期间其他线程调用了该线程的 interrupt()方法中断了该线程,则该线程会在调用 sleep 方法的地方抛出 InterruptedException 异常而返回。

下面举一个例子来说明,线程在睡眠时拥有的监视器资源不会被释放。

  1. public class SleepTest2 {
  2. // 创建一个独占锁
  3. private static final Lock lock = new ReentrantLock();
  4. public static void mainString[] args throws InterruptedException {
  5. // 创建线程 A
  6. Thread threadA = new Thread(new Runnable() {
  7. public void run() {
  8. // 获取独占锁
  9. lock.lock();
  10. try {
  11. System.out.println(「child threadA is in sleep」);
  12. Thread.sleep(10000);
  13. System.out.println(「child threadA is in awaked」);
  14. } catch InterruptedException e {
  15. e.printStackTrace();
  16. } finally {
  17. // 释放锁
  18. lock.unlock();
  19. }
  20. }
  21. });
  22. // 创建线程 B
  23. Thread threadB = new Thread(new Runnable() {
  24. public void run() {
  25. // 获取独占锁
  26. lock.lock();
  27. try {
  28. System.out.println(「child threadB is in sleep」);
  29. Thread.sleep(10000);
  30. System.out.println(「child threadB is in awaked」);
  31. } catch InterruptedException e {
  32. e.printStackTrace();
  33. } finally {
  34. // 释放锁
  35. lock.unlock();
  36. }
  37. }
  38. });
  39. // 启动线程
  40. threadA.start();
  41. threadB.start();
  42. }
  43. }

执行结果如下。

理解线程上下文切换 - 图1

如上代码首先创建了一个独占锁,然后创建了两个线程,每个线程在内部先获取锁,然后睡眠,睡眠结束后会释放锁。首先,无论你执行多少遍上面的代码都是线程 A 先输出或者线程 B 先输出,不会出现线程 A 和线程 B 交叉输出的情况。从执行结果来看,线程 A 先获取了锁,那么线程 A 会先输出一行,然后调用 sleep 方法让自己睡眠 10s,在线程 A 睡眠的这 10s 内那个独占锁 lock 还是线程 A 自己持有,线程 B 会一直阻塞直到线程 A 醒来后执行 unlock 释放锁。下面再来看一下,当一个线程处于睡眠状态时,如果另外一个线程中断了它,会不会在调用 sleep 方法处抛出异常。

  1. public static void mainString[] args throws InterruptedException {
  2. //创建线程
  3. Thread thread = new Thread(new Runnable() {
  4. public void run() {
  5. try {
  6. System.out.println(「child thread is in sleep」);
  7. Thread.sleep(10000);
  8. System.out.println(「child thread is in awaked」);
  9. } catch InterruptedException e {
  10. e.printStackTrace();
  11. }
  12. }
  13. });
  14. //启动线程
  15. thread.start();
  16. //主线程休眠 2s
  17. Thread.sleep(2000);
  18. //主线程中断子线程
  19. thread.interrupt();
  20. }

执行结果如下。

理解线程上下文切换 - 图2

子线程在睡眠期间,主线程中断了它,所以子线程在调用 sleep 方法处抛出了 InterruptedException 异常。

另外需要注意的是,如果在调用 Thread.sleep(long millis)时为 millis 参数传递了一个负数,则会抛出 IllegalArgumentException 异常,如下所示。

理解线程上下文切换 - 图3

让出 CPU 执行权的 yield 方法

Thread 类中有一个静态的 yield 方法,当一个线程调用 yield 方法时,实际就是在暗示线程调度器当前线程请求让出自己的 CPU 使用,但是线程调度器可以无条件忽略这个暗示。我们知道操作系统是为每个线程分配一个时间片来占有 CPU 的,正常情况下当一个线程把分配给自己的时间片使用完后,线程调度器才会进行下一轮的线程调度,而当一个线程调用了 Thread 类的静态方法 yield 时,是在告诉线程调度器自己占有的时间片中还没有使用完的部分自己不想使用了,这暗示线程调度器现在就可以进行下一轮的线程调度。

当一个线程调用 yield 方法时,当前线程会让出 CPU 使用权,然后处于就绪状态,线程调度器会从线程就绪队列里面获取一个线程优先级最高的线程,当然也有可能会调度到刚刚让出 CPU 的那个线程来获取 CPU 执行权。下面举一个例子来加深对 yield 方法的理解。

  1. public class YieldTest implements Runnable {
  2. YieldTest() {
  3. //创建并启动线程
  4. Thread t = new Threadthis);
  5. t.start();
  6. }
  7. public void run() {
  8. for int i = 0; i < 5; i++) {
  9. //当 i=0 时让出 CPU 执行权,放弃时间片,进行下一轮调度
  10. if ((i % 5) == 0 {
  11. System.out.println(Thread.currentThread() + yield cpu...」);
  12. //当前线程让出 CPU 执行权,放弃时间片,进行下一轮调度
  13. // Thread.yield();
  14. }
  15. }
  16. System.out.println(Thread.currentThread() + is over」);
  17. }
  18. public static void mainString[] args {
  19. new YieldTest();
  20. new YieldTest();
  21. new YieldTest();
  22. }
  23. }

输出结果如下。

理解线程上下文切换 - 图4

如上代码开启了三个线程,每个线程的功能都一样,都是在 for 循环中执行 5 次打印。运行多次后,上面的结果是出现次数最多的。解开 Thread.yield()注释再执行,结果如下。

理解线程上下文切换 - 图5

从结果可知,Thread.yield()方法生效了,三个线程分别在 i=0 时调用了 Thread.yield()方法,所以三个线程自己的两行输出没有在一起,因为输出了第一行后当前线程让出了 CPU 执行权。

一般很少使用这个方法,在调试或者测试时这个方法或许可以帮助复现由于并发竞争条件导致的问题,其在设计并发控制时或许会有用途,后面在讲解 java.util.concurrent. locks 包里面的锁时会看到该方法的使用。

总结:sleep 与 yield 方法的区别在于,当线程调用 sleep 方法时调用线程会被阻塞挂起指定的时间,在这期间线程调度器不会去调度该线程。而调用 yield 方法时,线程只是让出自己剩余的时间片,并没有被阻塞挂起,而是处于就绪状态,线程调度器下一次调度时就有可能调度到当前线程执行。