详细分析 Java 中启动线程的正确和错误方式

启动线程的正确和错误方式

前文回顾

  1. 详细分析 Java 中实现多线程的方法有几种?(从本质上出发)

start 方法和 run 方法的比较

代码演示:html

/**
 * <p>
 * start() 和 run() 的比较
 * </p>
 *
 * @author 踏雪彡寻梅
 * @version 1.0
 * @date 2020/9/20 - 16:15
 * @since JDK1.8
 */
public class StartAndRunMethod {
    public static void main(String[] args) {
        // run 方法演示
        // 输出: name: main
        // 说明由主线程去执行的, 不符合新建一个线程的本意
        Runnable runnable = () -> {
            System.out.println("name: " + Thread.currentThread().getName());
        };
        runnable.run();

        // start 方法演示
        // 输出: name: Thread-0
        // 说明新建了一个线程, 符合本意
        new Thread(runnable).start();
    }
}

从以上示例能够分析出如下两点:java

  • 直接使用 run 方法不会启动一个新线程。(错误方式)多线程

  • start 方法会启动一个新线程。(正确方式)ide

start 方法分析

start 方法的含义以及注意事项

  • start 方法能够启动一个新线程。源码分析

    • 线程对象在初始化以后调用了 start 方法以后, 当前线程(一般是主线程)会请求 JVM 虚拟机若是有空闲的话来启动一下这边的这个新线程。this

    • 也就是说, 启动一个新线程的本质就是请求 JVM 来运行这个线程。操作系统

    • 至于这个线程什么时候可以运行,并非简单的由咱们可以决定的,而是由线程调度器去决定的。线程

    • 若是它很忙,即便咱们运行了 start 方法,也不必定可以马上的启动线程。code

    • 因此说 srtart 方法调用以后,并不意味这个方法已经开始运行了。它可能稍后才会运行,也颇有可能很长时间都不会运行,好比说遇到了饥饿的状况。htm

    • 这也就印证了有些状况下,线程 1 先掉用了 start 方法,而线程 2 后调用了 start 方法,却发现线程 2 先执行线程 1 后执行的状况。

    • 总结: 调用 start 方法的顺序并不能决定真正线程执行的顺序。

    • 注意事项

      • start 方法会牵扯到两个线程。

      • 第一个就是主线程,由于咱们必需要有一个主线程或者是其余的线程(哪怕不是主线程)来执行这个 start 方法,第二个才是新的线程。

      • 不少状况下会忽略掉为咱们建立线程的这个主线程,不要误觉得调用了 start 就已是子线程去执行了,这个语句实际上是主线程或者说是父线程来执行的,被执行以后才去建立新线程。

  • start 方法建立新线程的准备工做

    • 首先,它会让本身处于就绪状态。

      • 就绪状态指已经获取到除了 CPU 之外的其余资源, 如已经设置了上下文、栈、线程状态以及 PC(PC 是一个寄存器,PC 指向程序运行的位置) 等。
    • 作完这些准备工做以后,就万事俱备只欠东风了,东风就是 CPU 资源。

    • 作完准备工做以后,线程才能被 JVM 或操做系统进一步去调度到执行状态等待获取 CPU 资源,而后才会真正地进入到运行状态执行 run 方法中的代码。

  • 须要注意: 不能重复的执行 start 方法

    • 代码示例

      /**
      * <p>
      * 演示不能重复的执行 start 方法(两次及以上), 不然会报错
      * </p>
      *
      * @author 踏雪彡寻梅
      * @version 1.0
      * @date 2020/9/20 - 16:47
      * @since JDK1.8
      */
      public class CantStartTwice {
          public static void main(String[] args) {
              Runnable runnable = () -> {
                  System.out.println("name: " + Thread.currentThread().getName());
              };
              Thread thread = new Thread(runnable);
              // 输出: name: Thread-0
              thread.start();
              // 输出: 抛出 java.lang.IllegalThreadStateException
              // 即非法线程状态异常(线程状态不符合规定)
              thread.start();
          }
      }
    • 报错的缘由

      • start 一旦开始执行,线程状态就从最开始的 New 状态进入到后续的状态,好比说 Runnable,而后一旦线程执行完毕,线程就会变成终止状态,而终止状态永远不可能再返回回去,因此会抛出以上异常,也就是说不能回到初始状态了。这里描述的还不够清晰,让咱们来看看源码能了解的更透彻。

start 方法源码分析

源码

public synchronized void start() {
    /**
     * This method is not invoked for the main method thread or "system"
     * group threads created/set up by the VM. Any new functionality added
     * to this method in the future may have to also be added to the VM.
     *
     * A zero status value corresponds to state "NEW".
     */
    // 第一步, 检查线程状态是否为初始状态, 这里也就是上面抛出异常的缘由
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    /* Notify the group that this thread is about to be started
     * so that it can be added to the group's list of threads
     * and the group's unstarted count can be decremented. */
    // 第二步, 加入线程组
    group.add(this);

    boolean started = false;
    try {
        // 第三步, 调用 start0 方法
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
              it will be passed up the call stack */
        }
    }
}

源码中的流程

第一步:
启动新线程时会首先检查线程状态是否为初始状态, 这也是以上抛出异常的缘由。即如下代码:

if (threadStatus != 0)
	throw new IllegalThreadStateException();

其中 threadStatus 这个变量的注释以下,也就是说 Java 的线程状态最初始(尚未启动)的时候表示为 0:

/* Java thread status for tools,
 * initialized to indicate thread 'not yet started'
 */
private volatile int threadStatus = 0;

第二步:
将其加入线程组。即如下代码:

group.add(this);

第三步:
最后调用 start0() 这个 native 方法(native 表明它的代码不是由 Java 实现的,而是由 C/C++ 实现的,具体实现能够在 JDK 里面看到,了解便可), 即如下代码:

boolean started = false;
try {
    // 第三步, 调用 start0 方法
    start0();
    started = true;
} finally {
    try {
        if (!started) {
            group.threadStartFailed(this);
        }
    } catch (Throwable ignore) {
        /* do nothing. If start0 threw a Throwable then
          it will be passed up the call stack */
    }
}

run 方法分析

run 方法源码分析

@Override
public void run() {
    // 传入了 target 对象(即 Runnable 接口的实现), 执行传入的 target 对象的 run 方法
    if (target != null) {
        target.run();
    }
}

对于 run 方法的两种状况

  • 第一种: 重写了 Thread 类的 run 方法,Threadrun 方法会失效, 将会执行重写的 run 方法。

  • 第二种: 传入了 target 对象(即 Runnable 接口的实现),执行 Thread 的原有 run 方法而后接着执行 target 对象的 run 方法。

  • 总结:

    • run 方法就是一个普通的方法, 上文中直接去执行 run 方法也就是至关于咱们执行本身写的普通方法同样,因此它的执行线程就是咱们的主线程。

    • 因此要想真正的启动线程,不能直接调用 run 方法,而是要调用 start 方法,其中能够间接的调用 run 方法。


若有写的不足的,请见谅,请你们多多指教。