接下来,我们主要分析多线程的生命周期是怎么样的。线程的生命周期至少包含了:创建、执行和终止三个过程。由于线程的复杂性,执行又分为执行、阻塞、等待、超时等待等过程。而终止也要考虑正常情况下的终止和阻塞情况的终止等问题。
本文,我们先来看如何创建线程。
创建线程的方式主要有三种:实现Runnable接口、继承Thread类或者实现Callable接口,我们一个个来看一下。当然还能使用线程池来创建,这个后面单独讨论。
Runnable是一个线程的接口,使用方法如下:
- public class RunnableTest implements Runnable {
- @Override
- public void run() {
- System.out.println("run ");
- }
-
- public static void main(String[] args) {
- Thread thread = new Thread(new RunnableTest());
- thread.start();
- System.out.println("main...");
- }
- }
注意启动线程要通过thead.start()来启动的,线程启动之后,会执行run()方法。为什么如此我们后面再讲解。
Thread是一个类,可以直接继承该类来创建线程,使用方法如下:
- public class ThreadTest extends Thread {
- @Override
- public void run() {
- System.out.println(" sun thread hello ");
- }
-
- public static void main(String[] args) {
- ThreadTest threadTest = new ThreadTest();
- threadTest.start();
- System.out.println("main thread");
-
- }
- }
Runnable和Thread的区别在于,前者是接口而后者是类,因此如果当前要实现线程的类已经继承了另外一个类,就无法再继承Thread类了,只能通过Runnable接口来完成。
在有些场景中,我们需要让一个异步执行的任务在执行完成后返回一个结果,而前面使用的线程中,run()方法是一个void修饰的无返回值方法,而前面两种方式都无法满足我们的要求,因此Java提供了Callable和Future组合创建返回值的线程。使用方法如下:
- public class CallableExample implements Callable<String> {
- @Override
- public String call() throws Exception {
- return "执行call";
- }
-
- public static void main(String[] args) throws ExecutionException, InterruptedException {
- CallableExample callableExample = new CallableExample();
- FutureTask<String> futureTask = new FutureTask<>(callableExample);
- Thread thread = new Thread(futureTask);
- thread.start();
- System.out.println("result" + futureTask.get());
- }
- }
可以看到Callable和Future组合还是略有复杂的,需要作为一个参数传递给thread。此外还可以传递给线程池来执行线程:
- public class ThreadPoolAndCallableTest implements Callable<String> {
-
- @Override
- public String call() throws Exception {
-
- Thread.sleep(2000);
- System.out.println("come in");
- return "SUCCESS";
- }
-
- public static void main(String[] args) throws ExecutionException, InterruptedException {
- ExecutorService executorService = Executors.newFixedThreadPool(1);
- ThreadPoolAndCallableTest callableDemo = new ThreadPoolAndCallableTest();
- System.out.println("main thread 1");
- Future<String> future = executorService.submit(callableDemo);
- System.out.println(future.get()); //阻塞,等处理完才回返回
- System.out.println("main thread 2");
- }
- }
这种方式在很多工程的源码中会看到,我们后面会单独分析其原理。
我们再来看一下线程创建的代码:
- Thread t1=new Thread()
- t1.start();//启动一个线程
- t1.run(); //这样仅仅是调用实例方法
start()方法被用来启动新创建的线程,这和直接调用run()方法的效果不一样,后者只是在原来的线程中进行了普通的方法调用,没有启动新的线程,start()方法才会启动新线程。 为什么呢,先看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) {
- ...
- }
- }
- }
可以看到最重要的是start0()方法,这个方法的实现是:
private native void start0();
标记为是一个native方法,之后就不能直接追踪了,需要看JVM的C++代码。 native方法是jvm调用的本地操作系统的C语言接口实现的方法,针对不同的操作系统平台,JVM给出了不同的实现。要知道jvm是c和C++实现的,native就是中间转了一下,直接调OS的多线程接口了看结构图。
我们在JVM课程里说过,JVM本身并不能处理多线程,而只是将用户的多线程做一定处理交给操作系统来执行。因为JVM本身是C++实现的,所以JVM也会通过C++来调用操作系统的线程接口,如下图所示。

当start()执行时启动线程时,会先在JVM层面创建一个线程, JVM具有适配各个平台的能力,因此会根据当前使用的操作系统类型来调用相关的指令创建一个操作系统级别的线程并启动。
但是线程启动之后并不会立即执行,也就不会直接执行run()方法内的代码。而是要操作系统的调度算法决定把当前线程分配给哪个CPU来执行。线程被分配执行之后,会回调线程中的run()方法执行相关指令。