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


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

    前文回顾

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

    start 方法和 run 方法的比较

    代码演示:

    /**
     * <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();
        }
    }
    

    从以上示例可以分析出以下两点:

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

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

    start 方法分析

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

    • start 方法可以启动一个新线程。

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

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

      • 至于这个线程何时能够运行,并不是简单的由我们能够决定的,而是由线程调度器去决定的。

      • 如果它很忙,即使我们运行了 start 方法,也不一定能够立刻的启动线程。

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

      • 这也就印证了有些情况下,线程 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 方法。


    如有写的不足的,请见谅,请大家多多指教。

  • 相关阅读:
    【科普杂谈】计算机按下电源后发生了什么
    【VS开发】使用WinPcap编程(1)——获取网络设备信息
    【VS开发】使用WinPcap编程(1)——获取网络设备信息
    微信公众平台消息接口PHP版
    编码gbk ajax的提交
    mysql 查询
    js cookie
    js同域名下不同文件下使用coookie
    去掉A标签的虚线框
    jquery切换class
  • 原文地址:https://www.cnblogs.com/txxunmei/p/13747631.html
Copyright © 2020-2023  润新知