⎡线程和线程池⎦2. 线程启动

265 阅读8分钟

创建线程的方式

为什么说本质上只有1种创建线程的方式?

创建线程的方式有几种?答案可能不尽相同,常见的有2种、4种等等。

其实不然,我们在使用多线程时,常见的至少有4种方式。但其创建线程的方式,有且只有1种,即通过构造一个Thread类来创建线程。

注意:在面试时,我们可以说使用多线程的方式常见的有4种,但它们的本质都是通过构造Thread类创建线程。

四种常见的使用方式

常见的4种使用方式如下:

  1. 继承Thread
  2. 实现Runnable,传给Thread作为构造参数
  3. 实现Runnable或者Callable,传递给线程池
  4. 实现Callable,构建FutureTask传给Thread

接下来,我们来分析这4种方法,来解释为什么本质上只有1种创建线程的方式。

先来看最基础的前两种方式:

继承Thread和实现Runnable的方式的本质

继承Thread

public class MyThread extends Thread {

    @Override
    public void run() {
        //TODO ...
    }
}

实现Runnable

public class MyRunableImpl implements Runnable {

    @Override
    public void run() {
        //TODO ...
    }
}

创建线程并运行

public static void main(String[] args) {
        Thread t1 = new MyThread();
        t1.start();
        Thread t2 = new Thread(new MyRunableImpl());
        t2.start();
    }

我们都知道,Thread#start方法运行后,经过一些操作会最终调用run()执行我们的操作,那么这两种方式是如何被调用到run()的呢?我们来查看Thread#run()。

public class Thread implements Runnable {

    private Runnable target;

    public Thread(Runnable target) {
        // 调用init方法,最终在init的重构方法中赋值给target成员变量,即this.target = target
        init(null, target, "Thread-" + nextThreadNum(), 0);    
    }

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

通过代码可以清楚的看到:

  • 当Runnable的成员变量不为空时,会通过构造函数传入Runnable实现类,new Thread后,就会执行实现类的run方法。
  • 当我们继承Thread类的,实际上是重写了Thread类中的run方法,new Thread后,run方法被调用时就执行了我们的逻辑。

但总的来说,这两种方式其实是一样的,都是通过构建Thread类创建线程。

线程池创建的本质

static class DefaultThreadFactory implements ThreadFactory {
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        private final ThreadGroup group;
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        private final String namePrefix;

        DefaultThreadFactory() {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() :
                                  Thread.currentThread().getThreadGroup();
            namePrefix = "pool-" +
                          poolNumber.getAndIncrement() +
                         "-thread-";
        }

        public Thread newThread(Runnable r) {
            Thread t = new Thread(group, r,
                                  namePrefix + threadNumber.getAndIncrement(),
                                  0);
            if (t.isDaemon())
                t.setDaemon(false);
            if (t.getPriority() != Thread.NORM_PRIORITY)
                t.setPriority(Thread.NORM_PRIORITY);
            return t;
        }
    }

这是很多线程池的默认线程工厂,当线程数小于核心线程时,newThread方法将被调用,从 newThread方法可以看出,将Runnable实现类的参数传给Thread作为构造参数,不过多了很多其他参数,但其实就是第2种。

实现Callable,构建FutureTask的本质

翻看源码,我们就知道FutureTask实现了Runnable接口,因此,作为一个Runnable接口的子类,传递给Thread作为构造参数,其实也是第2种。

综述,我们虽然发现了4种不同方式,但他们的原理都是一样的,即new Thread()或者new Thread(Runnable r)的方式,都是通过new一个Thread实例。

继承Thread、实现Runnable和Callable的区别

到这里,我们已经知道创建线程只有1种方式,但使用形式常见的有4种。但这4种又有共同点,归纳一下,我们可以分为3类:

  • 继承Thread
  • 实现Runnable
  • 实现Callable

那它们之间有什么区别呢?

实现Runnable接口比继承Thread好在哪里?

主要有3点:

  1. Java只支持单继承,所以继承Thread以后不能继承其他类
  2. 实现Runnable,该类作为1个任务类。Thread作为线程类,达到解耦的目的。
  3. 实现Runnable后,可以被多个线程共享并执行。

有了Runnable,为什么还需要Callable?

// Runnable
public interface Runnable {
    void run();
}

// Callable
public interface Callable<V> {
    V call() throws Exception;
}

通过上面的接口定义我们不难看出Runnable和Callable的区别:

  1. Runnable没有返回值,Callable使用泛型返回
  2. Runnable方法不能向上抛出异常,但Callable可以向上抛出异常

image-20200607233747470

当我们实现了Runnable方法,就不能向上抛出异常,只能使用try/catch包裹。

**那为什么Runnable要设计成不支持向上抛出异常呢?**主要是调用Runnable#run方法的类(比如Thread类和线程池)是Java直接提供的,不是我们编写,所以即使抛出异常,我们也不能捕获它们。

启动前的准备,创建线程

以无参构造函数为例,new Thread()只是调用了init(),该方法实际代码比较长,我挑了比较关键的出来。

private volatile int threadStatus = 0;

public Thread() {
        init(null, null, "Thread-" + nextThreadNum(), 0);
    }

public Thread(Runnable target) {
        init(null, target, "Thread-" + nextThreadNum(), 0);
    }

private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {

        this.name = name;

        // 设置父线程
        Thread parent = currentThread();

        this.group = g;
        // 继承父线程的守护属性
        this.daemon = parent.isDaemon();
        // 继承父线程的优先级
        this.priority = parent.getPriority();
        // classloader
        if (security == null || isCCLOverridden(parent.getClass()))
            this.contextClassLoader = parent.getContextClassLoader();
        else
            this.contextClassLoader = parent.contextClassLoader;
        this.target = target;
        setPriority(priority);
        // 设置 父线程的 inheritThreadLocals
        if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                    ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
        // 设置栈空间
        this.stackSize = stackSize;
        
        tid = nextThreadID();
    }

从代码中可以看出,new Thread()只是设置了线程相关属性,并没有向底层或操作系统申请线程。设置的属性包括父线程,父线程的守护属性、优先级、inheritThreadLocals以及栈空间等.这里还需要注意的是,当构造函数传入Runnable参数时,会将该值赋值给target变量,这是run()能执行创建线程时传入Runnable#run()的关键。

下一节我们会聊到Java的线程状态。代码的第一行,threadStatus变量默认被赋值为0,它代表了线程当前线程的状态为 NEWNEW 就是最开始的状态,表示线程被设置创建完成,但还没有启动(即还没有到 RUNNABLE )。

通过threadStatus获取线程状态的代码如下,了解即可。

public static State toThreadState(int var0) {
        if ((var0 & 4) != 0) {
            return State.RUNNABLE;
        } else if ((var0 & 1024) != 0) {
            return State.BLOCKED;
        } else if ((var0 & 16) != 0) {
            return State.WAITING;
        } else if ((var0 & 32) != 0) {
            return State.TIMED_WAITING;
        } else if ((var0 & 2) != 0) {
            return State.TERMINATED;
        } else {
            return (var0 & 1) == 0 ? State.NEW : State.RUNNABLE;
        }
    }

启动线程,start() & run()

对于Java程序员,我们只需要将线程需要运行的代码放到run()中,然后调用start()启动线程。Java会帮我们执行start()逻辑启动线程,start()中会调用run()方法来执行我们的代码。用起来就很方便了,那我们来看看这两个方法是如何配合完成我们的任务。

start()

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()方法主要干了这几件事:

  1. 判断threadStatus,在创建线程的时候说过,创建好线程时默认为0,因此才可以成功被调用,这也是为什么start()不能被重复执行的原因(面试题)。
  2. 添加该线程到线程组
  3. 调用start0()本地方法

start()执行完成过程中,会将threadStatus变为RUNNABLE,这是第2个Java线程状态,下一节将做详细介绍。

关于start0()本地方法的源码就不展开讲了,有兴趣的小伙伴推荐阅读这篇文章从 Java 到 C++, 以 JVM 的角度看 Java 线程的创建与运行 - Android - 掘金

该方法主要做的事情包括:

  1. 在VM中运行JVM_StartThread方法,创建一个JavaThread C对象。
  2. 在JavaThread对象创建过程中,创建一个OSThread对象,且JavaThread对象保持对OSThread对象的引用(作用是方便调用管理)。
  3. 在OSTread创建过程中,会创建一个平台相关的底层级线程,如果这个底层级线程创建失败,那么抛出异常。
  4. 底层级线程开始运行,并执行java.lang.Thread#run()。
  5. 当java.lang.Thread#run()执行完成或者中途抛出异常,则终止JavaThread
  6. 最后释放相关资源(C层面创建的如JavaThread及OSThread相关的内容、锁等)

run()

Thread#run()方法很简单,直接贴代码。

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

在创建线程时我们提到,构造函数在init(...)中会对target属性赋值。

  • 当通过继承Thread,调用无参构造方法时,target == null,因此执行实现类重写的run()。
  • 当通过调用有参构造方式时,便会调用Runnable#run()。

同时,我们可以看到,run()其实是一个很简单、很普通的方法,不像start()中对状态的判断,因此我们可以直接多次地调用run()。那直接多次调用run()的后果是什么呢?仅仅是run()中的代码在当前线程中多次被执行(这是一个面试题)。

start()和run()的区别

start() : 它的作用是启动一个新线程,新线程会执行相应的run()方法。start()不能被重复调用。

run() : run()就和普通的成员方法一样,可以被重复调用。单独调用run()的话,会在当前线程中执行run(),而并不会启动新线程!

面试题

以下面试题,请在正文中自行寻找答案

  1. Java创建线程有几种方式?最好的回答是解释清楚4种使用方式的本质是1种创建方式,一定要理解线程池的前提下,说明白线程池创建的本质,不然会给自己埋雷。
  2. 实现Runnable接口比继承Thread类的优点?
  3. 有Runnable接口,为什么还需要Callable?
  4. new一个线程做了什么?已经创建了OS级别的线程吗?
  5. start()和run()的实现细节
  6. 能否多次调用start()和run()?多次调用run()会出现什么情况?

参考文章

  1. 从 Java 到 C++, 以 JVM 的角度看 Java 线程的创建与运行 - Android - 掘金