目录
一、线程的创建和运行
1.1 创建和运行线程的三种方法
1.2 三者之间的继承关系
二、Thread类和Runnable接口的区别
2.1 Runnable接口可以实现线程之间资源共享,而Thread类不能
2.2 实现Runnable接口相对于继承Thread类的优点
三、实现 Runnable 接口和实现 Callable 接口的区别
四、Thread类和Runnable接口关于启动线程的源码解析
4.1 实现方法
4.2 Thread.start()方法源码分析
4.3 Runnable.run()方法源码分析
4.4 总结
一、线程的创建和运行
1.1 创建和运行线程的三种方法
Java里的程序天生就是多线程的,那么有几种启动线程的方式?
Java 线程创建有3种方式:
- 继承 Thread 类并且重写 run 方法
- 实现 Runnable接口的 run 方法
- 使用 Callable接口和FutureTask类方式
1.2 三者之间的继承关系
public class Thread implements Runnable {}
Thread类也是实现的Runnable接口。
单独说下 FutureTask 的方式,这种方式的本身也是实现了Runnable 接口的 run 方法,看它的继承结构就可以知道。
前两种方式都没办法拿到任务的返回结果,但是 Futuretask 方式可以。
由此我们知道了,其实这三种创建方法的根源,都是来源于Runnable接口,这三种方法往上层追溯都能追到Runnble接口。
二、Thread类和Runnable接口的区别
2.1 Runnable接口可以实现线程之间资源共享,而Thread类不能
实际上Thread类和Runnable接口之间在使用上也是有所区别的,如果一个类继承Thread类,就不适合于多个线程共享资源,而实现了Runnable接口,则可以方便的实现资源的共享。
由上文我们就可以知道,Thread类和Runnable接口最大的区别就是继承Thread类不能资源共享,而实现Runnable接口可以资源共享。
为什么Runnable可以共享数据:
总结起来原因就是用Runnable接口的方法可以对两个不同的Thread类的构造方法传入相同的实现Runnable接口的对象,那么这两个不同的Thread线程类本质操控的是同一个Runnable接口的实现对象了,调用的也是同一个run()方法,自然这两个线程下就实现了共享同一个Runnable实现类中的数据了。
如果两个Thread类的构造方法传入不同的Runnable接口实现类,那么两个Thread线程对象操作的不是同一个Runnable实现类,两个线程也就不能共享数据了。
2.2 实现Runnable接口相对于继承Thread类的优点
可见,实现Runnable接口相对于继承Thread类来说,有如下显著的优势:
- 适合多个相同程序代码的线程去处理同一资源的情况。
- 可以避免由于Java的单继承特性带来的局限。
- 增强了程序的健壮性,代码能够被多个线程共享,代码与数据是独立的。
- 线程池只能放入实现 Runable 或 Callable 类线程,不能直接放入继承 Thread 的类
三、实现 Runnable 接口和实现 Callable 接口的区别
- Runnable 是自从 java1.1 就有了,而 Callable 是 1.5 之后才加上去的
- 实现 Callable 接口的任务线程能返回执行结果,而实现 Runnable 接口的任务线程不能返回结果
- Callable 接口的 call()方法允许抛出异常,而 Runnable 接口的 run()方法的异常只能在内部消化,不能继续上抛
- 加入线程池运行,Runnable 使用 ExecutorService 的 execute 方法,Callable 使用 submit 方法。注:Callable 接口支持返回执行结果,此时需要调用 FutureTask.get()方法实现,此方法会阻塞主线程直到获取返回结果,当不调用此方法时,主线程不会阻塞
四、Thread类和Runnable接口关于启动线程的源码解析
这里我们来讲一下Java中创建线程最经典的这两种方式,在底层源码是如何实现的。
4.1 实现方法
Java 中实现多线程有两种「基本方式」:继承 Thread 类和实现 Runnable 接口。从实现的编程手法来看,认为这是两种实现方式并无不妥。但是究其实现根源,这么讲其实并不准确。
其实多线程从根本上讲只有一种实现方式,就是实例化 Thread,并且提供其执行的 run 方法。无论你是通过继承 Thread还是实现 Runnable接口,最终都是重写或者实现了 run 方法。而你真正启动线程都是通过实例化 Thread,调用其 start 方法。
来看下两种不同实现方式的例子:
1. 继承 Thread 方式:
public class MyThread extends Thread {
public void run() {
System.out.println("MyThread.run()");
}
}
MyThread myThread1 = new MyThread();
MyThread myThread2 = new MyThread();
myThread1.start();
myThread2.start();
2. 实现 Runnable 方式
public class MyThread extends OtherClass implements Runnable {
public void run() {
System.out.println("MyThread.run()");
}
}
MyThread myThread = new MyThread();
Thread thread = new Thread(myThread);
thread.start();
第一种方式中,MyThread 继承了 Thread 类,启动时调用的 start 方法,其实还是他父类 Thread 的 start 方法。并最终触发执行 Student 重写的 run 方法。
第二种方式中,MyThread 实现 Runnable 接口,将MyThread对象作为参数传递给 Thread 构造函数。接下来还是调用了 Thread 的 start 方法。最后则会触发传入的 Runnable 实现类的 run 方法。
两种方式都是创建 Thread 或者 Thread 的子类,通过 Thread 的 start 方法启动。唯一不同是第一种 run 方法实现在 Thread 子类中。第二种则是把 run 方法逻辑转移到 Runnable 的实现类中。线程启动后,第一种方式是 thread 对象运行自己的 run 方法逻辑,第二种方式则是调用 Runnable 实现的 run 方法逻辑。
相比较来说,第二种方式是更好的实践,原因如下:
- java 语言中只能单继承,通过实现接口的方式,可以让实现类去继承其它类。而直接继承 thread 就不能再继承其它类了;
- 线程控制逻辑在 Thread 类中,业务运行逻辑在 Runnable 实现类中。解耦更为彻底;
- 实现 Runnable 的实例,可以被多个线程共享并执行。而实现 thread 是做不到这一点的。
看到这里,你是不是很好奇,为什么程序中调用的是 Thread 的 start 方法,而不是 run 方法?为什么线程在调用 start 方法后会执行 run 方法的逻辑呢?接下来我们通过学习 start 方法的源代码来找到答案。
4.2 Thread.start()方法源码分析
Thread类的无参构造方法:
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
如果是直接创建Thread类对象,我们通过源码就能看出,传入到target是空。在这种情况下,我们需要在Thread的继承类中去覆写run()方法,这样在Thread类执行run()方法的时候,就是调用我们继承类中覆写的run()方法逻辑。
我们知道Thraed类的对象是不能直接调用run()方法的,那么它是如何调用run()方法的呢?下面我们接着来进行分析。
Thread类中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) {
}
}
}
这段代码足够简单,简单到没什么内容。主要逻辑如下:
- 检查线程的状态,是否可以启动;
- 把线程加入到线程 group 中;
- 调用了 start0 () 方法。
可以看到 Start 方法中最终调用的是 start0()方法,并不是 run 方法。那么我们再看 start0 方法源代码:
private native void start0();
什么也没有,因为 start0 是一个 native 方法,也称为 JNI(Java Native Interface)方法。JNI 方法是 Java和其它语言交互的方式。同样也是 Java代码和虚拟机交互的方式,虚拟机就是由 C++ 和汇编所编写。
由于 start0 是一个 native 方法,所以后面的执行会进入到 JVM 中。那么 run 方法到底是何时被调用的呢?这里似乎找不到答案了。
难道我们错过了什么?回过头来我们再看看 Start 方法的注解。其实读源代码的时候,要先读注解,否则直接进入代码逻辑,容易陷进去,出不来。原来答案就在 start 方法的注解里,我们可以看到:
/*
* Causes this thread to begin execution; the Java Virtual Machine* calls the run method of this thread.*
* The result is that two threads are running concurrently: the
* current thread (which returns from the call to the
* start method) and the other thread (which executes its
* run method).
*
*
* It is never legal to start a thread more than once.
* In particular, a thread may not be restarted once it has completed execution.
*/
最关键一句: the Java Virtual Machine calls the run method of this thread。由此我们可以推断出整个执行流程如下:
start 方法调用了 start0 方法,start0 方法在 JVM 中,start0 中的逻辑会调用 run 方法。
至此,我们已经分析清楚从线程创建到 run 方法被执行的逻辑。但是通过实现 Runnbale 的方式实现多线程时,Runnable 的 run 方法是如何被调用的呢?
4.3 Runnable.run()方法源码分析
我们先从 Thread 的构造函数入手。原因是 Runnable 的实现对象通过构造函数传入 Thread。
Thread类构造方法源码:
public Thread(Runnable target) {
init(null, target, "Thread-" + nextThreadNum(), 0);
}
可以看到 Runnable 实现作为 target 对象传递进来。再次调用了 init 方法,init 方法有多个重载,最终调用的是Thread类中的如下方法:
private void init(ThreadGroup g, Runnable target, String name, long stackSize) {
Thread parent = currentThread();
if (g == null) {
g = parent.getThreadGroup();
}
g.addUnstarted();
this.group = g;
this.target = target;
this.priority = parent.getPriority();
this.daemon = parent.isDaemon();
setName(name);
init2(parent);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
tid = nextThreadID();
}
此方法里有一行代码:
this.target = target;
原来 target 是 Thread类的成员变量:
/* What will be run. */
private Runnable target;
此时,Thread 的 target 被设置为你实现业务逻辑的 Runnable 实现。
我们再看下Thread类的run 方法的代码:
@Override
public void run() {
if (target != null) {
target.run();
}
}
看到这里是不是已经很清楚了,当你传入了 target时(target不为null),在执行Thread类的run()方法时其实会调用执行 target 的 run 方法。也就是执行你实现业务逻辑的方法,我们需要在实现Runnable接口的类中实现Runnable接口的run()方法。整体执行流程如下:
如果你是通过继承 Thread,重写 run 方法的方式实现多线程。那么在上图中的第三步执行的就是你重写的 run 方法。
我们回过头看看 Thread 类的定义:
public class Thread implements Runnable
原来 Thread 也实现了 Runnable 接口。怪不得 Thread 类的 run 方法上有 @Override 注解。所以继承 Thread类实现多线程,其实也相当于是实现 Runnable 接口的 run 方法。只不过此时,不需要再传入一个 Thread 类去启动。它自己已具备了 Thread 的功能,自己就可以运转起来。既然 Thread 类也实现了 Runnable 接口,那么 Thread 子类对象是不是也可以传入另外的 Thread 对象,让其执行自己的 run 方法呢?答案是可行的。
4.4 总结
以上对多线程的两种实现方式做了分析。在学习多线程的同时,我们也应该学习源代码中优秀的设计模式。Java 中多线程的实现采用了模板模式。Thread 是模板对象,负责线程相关的逻辑,比如线程的创建、运行以及各种操作。而线程真正的业务逻辑则被剥离出来,交由 Runnable 的实现类去实现。线程操作和业务逻辑完全解耦,普通开发者只需要聚焦在业务逻辑实现。
执行业务逻辑,是 Thread 对象的生命周期中的重要一环。这一步通过调用传入 Runnable 的 run 方法实现。Thread 线程整体逻辑就是一个模板,把其中一个步骤剥离出来由其他类实现,这就是模板模式。
相关文章:【并发基础】线程,进程,协程的详细解释
【操作系统】一篇文章带你快速搞懂用户态和内核态