1、构造线程

在运行线程之前首先要构造一个线程对象,线程对象在构造的时候需要提供线程所需的属性,如线程所属的线程组、线程优先级、是否是 Daemon 线程等信息。下面是 java.lang.Thread 中对线程初始化的代码:

  1. /**
  2. * Initializes a Thread.
  3. *
  4. * @param g the Thread group
  5. * @param target the object whose run() method gets called
  6. * @param name the name of the new Thread
  7. * @param stackSize the desired stack size for the new thread, or
  8. * zero to indicate that this parameter is to be ignored.
  9. * @param acc the AccessControlContext to inherit, or
  10. * AccessController.getContext() if null
  11. * @param inheritThreadLocals if {@code true}, inherit initial values for
  12. * inheritable thread-locals from the constructing thread
  13. */
  14. private void init(ThreadGroup g, Runnable target, String name,
  15. long stackSize, AccessControlContext acc,
  16. boolean inheritThreadLocals) {
  17. if (name == null) {
  18. throw new NullPointerException("name cannot be null");
  19. }
  20. this.name = name;
  21. // 当前线程就是该线程的父线程
  22. Thread parent = currentThread();
  23. SecurityManager security = System.getSecurityManager();
  24. if (g == null) {
  25. /* Determine if it's an applet or not */
  26. /* If there is a security manager, ask the security manager
  27. what to do. */
  28. if (security != null) {
  29. g = security.getThreadGroup();
  30. }
  31. /* If the security doesn't have a strong opinion of the matter
  32. use the parent thread group. */
  33. if (g == null) {
  34. g = parent.getThreadGroup();
  35. }
  36. }
  37. /* checkAccess regardless of whether or not threadgroup is
  38. explicitly passed in. */
  39. g.checkAccess();
  40. /*
  41. * Do we have the required permissions?
  42. */
  43. if (security != null) {
  44. if (isCCLOverridden(getClass())) {
  45. security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION);
  46. }
  47. }
  48. g.addUnstarted();
  49. this.group = g;
  50. // 将父线程的 daemon、priority 属性赋值给该线程对应的属性
  51. this.daemon = parent.isDaemon();
  52. this.priority = parent.getPriority();
  53. if (security == null || isCCLOverridden(parent.getClass()))
  54. this.contextClassLoader = parent.getContextClassLoader();
  55. else
  56. this.contextClassLoader = parent.contextClassLoader;
  57. this.inheritedAccessControlContext =
  58. acc != null ? acc : AccessController.getContext();
  59. this.target = target;
  60. setPriority(priority);
  61. // 将父线程的 InheritableThreadLocal 复制
  62. if (inheritThreadLocals && parent.inheritableThreadLocals != null)
  63. this.inheritableThreadLocals =
  64. ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
  65. /* Stash the specified stack size in case the VM cares */
  66. this.stackSize = stackSize;
  67. /* Set thread ID */
  68. tid = nextThreadID();
  69. }

一个新构造的线程对象由其 parent 线程来进行空间分配的,而子线程继承了 parent 线程的 Daemon、priority、contextClassLoader 以及 inheritableThreadLocals(可继承的 ThreadLocal),同时还分配了一个唯一的 ID 来标识这个子线程。此时,一个能够运行的线程对象就初始化好了,在堆内存中等待着运行。

2、启动线程

线程对象在初始化完成之后,调用 start 方法就可以启动这个线程。线程 start 方法的含义:当前线程(parent 线程)同步告知 JVM,只要线程规划器空闲,应立即启动调用了 start 方法的线程。

  1. package com.yj.thread;
  2. /**
  3. * @description: 启动线程
  4. * @author: erlang
  5. * @since: 2021-02-02 20:12
  6. */
  7. public class StartThread {
  8. public static void main(String[] args) {
  9. new Thread(new Runner(), "ThreadName-Test-1").start();
  10. new Thread(new Runner()).start();
  11. }
  12. private static class Runner implements Runnable {
  13. @Override
  14. public void run() {
  15. while (true) {
  16. }
  17. }
  18. }
  19. }

运行 StartThread 代码后,用 jstack 查看堆栈信息,结果如图所示。从图中我们可以知道,在启动一个线程前,最好为这个线程设置线程名称,因为这样在使用 jstack 分析程序或者进行问题排查时,就回给开发人员提供一些提示,自定义线程最好能够起个名字

五、启动和终止线程 - 图1

3、中断

中断可以理解为线程的一个标识位属性,它表示一个运行中的线程是否被其他线程进行了中断操作。中断好比其他线程对该线程打了个招呼,其他线程通过调用该线程的 interrupt 方法对其进行中断操作。

线程通过检查自身是否被中断来进行响应,线程通过方法 isInterrupted 来进行判断是否被中断,也开一个调用静态方法 Thread::interrupted 方法对当前线程的中断标识进行复位。如果线程已经处于总结状态,即使该线程被中断过,在调用该线程对象的 isInterrupted 方法时依旧返回 false。

从 Java 的 API 中可以看到,许多声明抛出 InterruptedException 的方法,如 Thread::sleep 方法,在抛出 InterruptedException 之前,JVM 会先将该线程的中断标识清除,然后抛出 InterruptedException,此时调用个 isInterrupted 方法将会放回 false。示例代码如下

  1. package com.yj.thread;
  2. import java.util.concurrent.TimeUnit;
  3. /**
  4. * @description: 中断线程
  5. * @author: erlang
  6. * @since: 2021-02-02 20:29
  7. */
  8. public class InterruptedThread {
  9. public static void main(String[] args) throws InterruptedException {
  10. // sleepThread 设置一个很大的睡眠时间
  11. Thread sleepThread = new Thread(new SleepRunner(), "SleepThread");
  12. // loopThread 不停的运行
  13. Thread loopThread = new Thread(new LoopRunner(), "LoopThread");
  14. sleepThread.start();
  15. loopThread.start();
  16. TimeUnit.SECONDS.sleep(5);
  17. sleepThread.interrupt();
  18. loopThread.interrupt();
  19. System.out.println("SleepThread interrupted is " + sleepThread.isInterrupted());
  20. System.out.println("LoopThread interrupted is " + loopThread.isInterrupted());
  21. }
  22. static class SleepRunner implements Runnable {
  23. @Override
  24. public void run() {
  25. try {
  26. TimeUnit.SECONDS.sleep(100000);
  27. } catch (InterruptedException e) {
  28. e.printStackTrace();
  29. }
  30. }
  31. }
  32. static class LoopRunner implements Runnable {
  33. @Override
  34. public void run() {
  35. while (true) {
  36. }
  37. }
  38. }
  39. }

输出结果如下,线程 SleepThread 抛出 InterruptedException 中断睡眠,其中中断标识位也被清除了;而 LoopThread 则一直在运行没有中断,中断标识位没有被清除。

  1. SleepThread interrupted is false
  2. LoopThread interrupted is true
  3. java.lang.InterruptedException: sleep interrupted
  4. at java.lang.Thread.sleep(Native Method)
  5. at java.lang.Thread.sleep(Thread.java:340)
  6. at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
  7. at com.yj.thread.InterruptedThread$SleepRunner.run(InterruptedThread.java:32)
  8. at java.lang.Thread.run(Thread.java:748)

4、过期的方法

线程的暂定,恢复和停止操作对应的分别是 suspend,resume,stop 方法。这三个方法目前都已被废弃,具体原因后面会介绍。这里先看个例子,代码如下:

  1. package com.yj.thread;
  2. import java.time.LocalDateTime;
  3. import java.time.format.DateTimeFormatter;
  4. import java.util.concurrent.TimeUnit;
  5. /**
  6. * @description: 线程的废弃方法演示
  7. * @author: erlang
  8. * @since: 2021-02-02 20:56
  9. */
  10. public class DeprecatedMethodThread {
  11. private final static DateTimeFormatter formatter = DateTimeFormatter.ofPattern("HH:mm:ss");
  12. public static void main(String[] args) throws InterruptedException {
  13. Thread deprecatedThread = new Thread(new Runner(), "DeprecatedThread");
  14. deprecatedThread.start();
  15. TimeUnit.SECONDS.sleep(3);
  16. // 将 DeprecatedThread 进行暂停,输出内容
  17. deprecatedThread.suspend();
  18. System.out.println("main suspend DeprecatedThread at " + LocalDateTime.now().format(formatter));
  19. TimeUnit.SECONDS.sleep(3);
  20. // 将 DeprecatedThread 进行恢复,输出内容
  21. deprecatedThread.resume();
  22. System.out.println("main resume DeprecatedThread at " + LocalDateTime.now().format(formatter));
  23. TimeUnit.SECONDS.sleep(3);
  24. // 将 DeprecatedThread 进行停止,输出内容停止
  25. deprecatedThread.stop();
  26. System.out.println("main stop DeprecatedThread at " + LocalDateTime.now().format(formatter));
  27. }
  28. static class Runner implements Runnable {
  29. @Override
  30. public void run() {
  31. while (true) {
  32. System.out.println(Thread.currentThread().getName() + " Run at " + LocalDateTime.now().format(formatter));
  33. try {
  34. TimeUnit.SECONDS.sleep(1);
  35. } catch (InterruptedException e) {
  36. e.printStackTrace();
  37. }
  38. }
  39. }
  40. }
  41. }

输出结果如下,通过结果可以看到 suspend、resume 和 stop 三个方法分别完成了线程的暂停、恢复和终止工作。但这些 API 是过期的,不建议使用的。不建议的原因不建议的原因不建议的原因不建议的原因不建议的原因主要有:在调用suspend 方法后,线程不会释放已经占有的资源,而是占有着资源进入睡眠暂状态,这样容易造成死锁问题;stop 方法在终结一个线程时不会保证的资源正常释放,通常是没有给予线程完全资源释放工作的机会,因此会导致程序可能工作在不确定状态下。

  1. DeprecatedThread Run at 21:17:06
  2. DeprecatedThread Run at 21:17:07
  3. DeprecatedThread Run at 21:17:08
  4. main suspend DeprecatedThread at 21:17:09
  5. main resume DeprecatedThread at 21:17:12
  6. DeprecatedThread Run at 21:17:12
  7. DeprecatedThread Run at 21:17:13
  8. DeprecatedThread Run at 21:17:14
  9. main stop DeprecatedThread at 21:17:15

5、安全地终止线程

前面有提到中断操作是一种简便的线程间交互方式,而这种交互方式最适合用来取消或停止任务。除了中断以外还可以利用一个 boolean 变量来控制是否需要停止任务并终止该线程。示例代码如下:

  1. package com.yj.thread;
  2. import java.util.concurrent.TimeUnit;
  3. /**
  4. * @description: 终止线程
  5. * @author: erlang
  6. * @since: 2021-02-02 21:56
  7. */
  8. public class ShutdownThread {
  9. public static void main(String[] args) throws InterruptedException {
  10. Thread countThread = new Thread(new Runner(), "CountThread-One");
  11. countThread.start();
  12. TimeUnit.SECONDS.sleep(1);
  13. countThread.interrupt();
  14. // 查看中断状态
  15. System.out.println(countThread.getName() + " isInterrupted: " + countThread.isInterrupted());
  16. Runner runner = new Runner();
  17. countThread = new Thread(runner, "CountThread-Two");
  18. countThread.start();
  19. TimeUnit.SECONDS.sleep(1);
  20. runner.cancel();
  21. }
  22. static class Runner implements Runnable {
  23. private long count;
  24. private volatile boolean on = true;
  25. @Override
  26. public void run() {
  27. while (on && !Thread.currentThread().isInterrupted()) {
  28. count++;
  29. }
  30. System.out.println(Thread.currentThread().getName() + " Count is " + count);
  31. }
  32. public void cancel() {
  33. on = false;
  34. }
  35. }
  36. }

输出结果如下,main 线程通过中断操作和 cancel 方法均可使得 CountThread 安全终止。这种通过标识位或者中断操作的方式能够使线程在终止时有机会去清理资源,而不是立刻结束线程,因此这种方式更加安全和优雅。

  1. CountThread-One isInterrupted: true
  2. CountThread-One Count is 744025473
  3. CountThread-Two Count is 783726399

上面的代码中,判断线程是否中断有两种方式 Thread.currentThread().isInterrupted () 和 Thread.interrupted(),源代码如下。从源代码中我们可以看出, isInterrupted 不会清除线程中断的状态,interrupted 会清除线程的中断状态。

  1. public static boolean interrupted() {
  2. return currentThread().isInterrupted(true);
  3. }
  4. public boolean isInterrupted() {
  5. return isInterrupted(false);
  6. }
  7. /**
  8. * Tests if some Thread has been interrupted. The interrupted state
  9. * is reset or not based on the value of ClearInterrupted that is
  10. * passed.
  11. */
  12. private native boolean isInterrupted(boolean ClearInterrupted);