关于线程的启动正确姿势详解编程语言

导语

在面试中,很多童鞋可能被问到过start方法和run方法的区别,貌似这种问题都被问烂了,笔者貌似也被问过,不记得当初是怎么回答的了,但肯定回答的怎么样。那么对于这样的问题,我们应该怎么回答呢?

当然首先肯定要从理论层面说一下,然后从代码层面说一下,但是理论还是要借助于代码实现来衬托的,所以本篇文章大体思路应该是先从代码实现上比较一下,然后对每个方法的基本实现进行一个简单的阐述。注:本篇文章只基于java代码实现来说,不基于JVM源码。

一、start方法与run方法的比较

对于start方法和run方法的比较,我们还是先从代码上来看,如下:

public class StartAndRunThreadDemo { 
    public static void main(String[] args) { 
        Runnable runnable = () -> { 
            System.out.println(Thread.currentThread().getName()); 
        }; 
        runnable.run(); 
        new Thread(runnable).start(); 
//        Thread thread = new Thread(new Runnable() { 
//            @Override 
//            public void run() { 
//                System.out.println(Thread.currentThread().getName() + " invoked..."); 
//            } 
//        }); 
//        thread.start(); 
//        thread.run(); 
    } 
} 

上面代码的执行结果如下:

"C:/Program Files/Java/jdk1.8.0_131/bin/java.exe" ... 
main 
Thread-0 

从执行结果上,我们可以得知runnable.run()方法的执行结果是表示它是main线程所执行的,而start方法是由子线程去执行的,可是run()方法由主线程所执行的,这便不符合多线程环境下所具有的意义了。那我们的本意是什么呢?我们的本意是要新建一个线程然后把它启动起来。其实就是因为run方法它只是一个普通的java方法,它并不具有启动线程的作用。为什么这么说呢?我们来看下官网是如何阐述的:

关于线程的启动正确姿势详解编程语言

上面的意思用简单的话来说就是:如果是直接调用Thread类中的run方法,而不不覆写该方法,那将没有上面实质的结果,虽然Thread类中的run方法实现了Runable接口,但是其本身并不具有启动线程的意义,这和Runable接口中的run方法没有什么实质的区别。对此我们后面还将进行说明。

二、start方法的(执行)原理

 

1.start方法的含义:

(1)线程的启动

首先来说start方法的含义是具有启动线程的作用的,而这里所说的线程便是我们常说的主线程,对此我们依旧以官网的解释为主,如下:

关于线程的启动正确姿势详解编程语言

大体意思就是:当先处对象初始化后执行start方法,然后当前线程(主线程)去通知JVM虚拟机,如果虚拟机有时间的的话来执行新的线程,也就是Java虚拟机调用这个线程的run方法,此时的结果便是两个线程同时运行:当前线程(主线程)(从调用返回到start方法)和另一个线程(子线程)(执行其run方法)。

从上面我们知道,启动一个线程的本质就是通知JVM来运行一个线程,对于这个线程何时运行,就需要线程调度器去决定了,这个意思也就是说嗲用start方法的顺序并不能决定线程的运行顺序。

(2)具体准备工作

上面提到了另一个线程,就是子线程,后面统一用子线程来说明,但是子线程的运行时需要一些准备的,譬如汽车起动前需要:加满汽油、加满机油、保证蓄电池电量充足、启动开关没坏等等。线程运行前的准备也是如此,因此首先是需要使其处于就绪状态,就绪状态的意思是获得到了CPU以外的其他资源,譬如设置上下文、栈、线程状态以及寄存器,寄存器指向了程序的运行位置。当昨晚完以上操作后,子线程才能进一步被JVM调度到执行状态,调度到执行状态后,如果获得到了CPU资源,此时才会真正的进入到运行状态。其实这也可以看做是你开着一辆车行驶在马路上,你启动了汽车不代表你就可以跑了,而是要依赖于马路上的红绿灯。红绿灯就相当于JVM的调度作用了。

(3)两次调用start方法的结果

上面的官网截图中说:多次启动一个线程是不合法的。特别是,线程在完成执行后可能不会重新启动。意思就是说不可重复执行start方法。对此就不做代码演示了,感兴趣的童鞋可以自己去试试。但是我们会在下面的源码讲述中进行简单的说明。

2.start源码解析

关于start方法代码的执行可以简单分为以下几个步骤:

  • 启动时检查线程状态
  • 添加到线程组
  • 调用本地方法start0

那么我们还是来看下代码中是如何实现的吧,如下:

    public synchronized void start() { 
 
        if (threadStatus != 0) 
            throw new IllegalThreadStateException(); 
 
        group.add(this); 
 
        boolean started = false; 
        try { 
            start0(); 
            started = true; 
        } finally { 
            try { 
                if (!started) { 
                    group.threadStartFailed(this); 
                } 
            } catch (Throwable ignore) { 
            } 
        } 
    }

从上面的代码中,可以看出其大体执行步骤的确如我们上面所说的,首先是建厂线程状态,这也就是我们前面所说的重复执行start方法时,便会在此进行校验。然后便是添加线程组,我们也来看一下此步操作的代码:

    void add(Thread t) { 
        synchronized (this) { 
            if (destroyed) { 
                throw new IllegalThreadStateException(); 
            } 
            if (threads == null) { 
                threads = new Thread[4]; 
            } else if (nthreads == threads.length) { 
                threads = Arrays.copyOf(threads, nthreads * 2); 
            } 
            threads[nthreads] = t; 
            nthreads++; 
            nUnstartedThreads--; 
        } 
    }

其实此时我们应该考虑这样有个问题,我们只调用start方法,那么我们在哪里运行线程呢?那我们继续看下start0()方法的实现:

    private native void start0();

此时你会不会很失望,native方法便是虚拟机内的方法了,前面我们说了本篇文章不涉及JVM源码,所以这里就不做详细的代码展示了。

三、run方法原理

run方法的代码我们前面的文章已经做了一些展示,也就那么几行,这里我们还是贴一下代码吧,如下:

    public void run() { 
        if (target != null) { 
            target.run(); 
        } 
    }

其实我们前面也说了为什么我们启动一个线程必须嗲用start方法,而不是run方法,这里从代码中我们也可以看出,run方法就是个普通的java方法,并没有实质性的操作系统的意义,而调用start方法是要调用jvm源码的,在jvm源码中会调用其相应的run方法的。这里就不赘述了。

 

总结

本篇文章进对run方法和start方法进行了简单的对比,其意义是要体现启动线程的正确姿势。我们文章的大体脉络如下:

  • start方法与run方法的比较
  • start方法的原理
  • run方法的原理

 

原创文章,作者:奋斗,如若转载,请注明出处:https://blog.ytso.com/19406.html

(0)
上一篇 2021年7月19日
下一篇 2021年7月19日

相关推荐

发表回复

登录后才能评论