0%

Java 并发 - 多线程:线程创建的几种方式

1.简介

在这阶段的学习过程中我会先抛出一系列:线程如何创建?这是一个很关键的问题,并发的关键在于多线程,那么如何创建线程呢?大概有几种方式呢?这几种方式的区别是什么?什么情况下应该使用这种创建方式?什么时候又不应该呢?那么具体的过程应该是如何呢?是否应该给出一两个例子会更好的说明一下?

问题太多,搞得自己都乱了,最主要的还是要一点点的去了解,最后串成一根线,才能更好对知识的进行掌握。

我想应该将这几种方式联系起来做一个对比,这样才能更好的理解这些创建线程方式的优点与缺点。

按照现有的认识,总的来说有两种实现线程的方式:

  1. 实现Runnable接口
  2. 继承Thread类

其实按照我的理解的话,详细分一下的话可以分为三种,就是继承Thread类,实现Runnable接口,实现Callable接口(虽然其内部也是实现Runnable接口),主要就是实现Runnable接口没有返回值,而实现Callable接口可以有返回值,所以也可以按照这三种方式去思考实际开发过程中到底需要哪种创建方式。


2.几种实现方式详解

2.1 继承Thread类

实现:

  1. 需要实现 run() 方法,因为 Thread 类也实现了 Runable 接口。
  2. 当调用 start() 方法启动一个线程时,虚拟机会将该线程放入就绪队列中等待被调度,当一个线程被调度时会执行该线程的 run() 方法。

Demo如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Demo {
// 注意就是这里extend 进行继承了Thread
public static class MyThread extends Thread {
// 重写run方法,需要线程做的事情在这里进行编辑!
@Override
public void run(){
System.out.printl("MyThread");
}
}

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

关于start()方法需要注意的有两点:

  1. 我们在程序里面调用了start()方法后,虚拟机会先为我们创建一个线程,然后等到这个线程第一次得到时间片时再调用run()方法。
  2. 注意不可多次调用start()方法。在第一次调用start()方法后,再次调用start()方法会抛出异常。

此处我有两个疑惑,根据平时的积累之后给出了答案,问题如下:

  1. start()方法和run()方法的区别?

    • 只有调用了start()方法,才会表现出多线程的特性,不同线程的run()方法里面的代码交替执行。如果只是调用run()方法,那么代码还是同步执行的,必须等待一个线程的run()方法里面的代码全部执行完毕之后,另外一个线程才可以执行其run()方法里面的代码。
  2. 为什么我们调用 start() 方法时会执行 run() 方法,为什么我们不能直接调用 run() 方法?

    • new 一个 Thread,线程进入了新建状态;调用 start() 方法,会启动一个线程并使线程进入了就绪状态,当分配到时间片后就可以开始运行了。 start() 会执行线程的相应准备工作,然后自动执行 run() 方法的内容,这是真正的多线程工作。 而直接执行 run() 方法,会把 run 方法当成一个 main 线程下的普通方法去执行,并不会在某个线程中执行它,所以这并不是多线程工作。
    • 总结: 调用 start 方法方可启动线程并使线程进入就绪状态,而 run 方法只是 thread 的一个普通方法调用,还是在主线程里执行。

2.2 实现Runnable接口

实现:

  1. 实现接口方式
    • 需要实现接口中的 run() 方法。
    • 使用 Runnable 实例再创建一个 Thread 实例,然后调用 Thread 实例的 run() 方法来启动线程。
  2. 函数式编程实现方式(JDK 1.8 +)
    • 可以使用函数式编程:new Thread(() -> { .... }).start();方便快捷!

Runnable是一个函数式接口,这意味着我们可以使用Java 8的函数式编程来简化代码。

首先还是来认识一下函数式编程是个什么东西吧?

函数式编程这是在Java 8 之后才有的,它的声明是通过一个注解来实现的,可以查看Runnable 的接口实现便可知道

1
2
3
4
@FunctionalInterface
public interface Runnable {
public abstract void run();
}

Demo如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Demo {
// 1.实现接口的方式
public static class MyThread implements Runnable {
@Override
public void run(){
System.out.println("MyThread");
}
}

public static void main(String[] args){
// 需要注意的是这里是run而不是start!
new MyThread().run();
// 2.函数式编程
// Java 8 之后的函数式编程如下
new Thread(() -> {
Ststem.out.println("Java 8 匿名内部类");
}).start();
}
}

输出如下:

1
2
MyThread
Java 8 匿名内部类

需要注意的是实现了Runnable接口(implements Runnable)要进行使用的时候是使用new MyThread().run();而不是new Thread().start();

所以实现Runnable接口,我们有两种方式可以去实现创建线程,总的来说,使用匿名内部类的函数式编程会比较方便一点,不用那么多操作,那当然什么方便使用什么了~

上面的实现接口方式还可以这么写,都是一样的,就是知道多个写法

  1. 需要实现接口中的 run() 方法。
  2. 使用 Runnable 实例再创建一个 Thread 实例,然后调用 Thread 实例的 start() 方法来启动线程。

Demo如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class MyRunnable implements Runnable{
@Override
public void run(){
// ....
}
}

public static void main(String[] args){
MyRunnable instance = new MyRunnable();
// 将这个实现Runnable 接口的实例传递进来
Thread thread = new Thread(instance);
// 调用thread的start 方法
// 注意和上面的实现方式对比,不是run方法
thread.start();
}

2.3 实现 Callable接口

2.3.1 Callable接口

其实也算是实现Runnable接口!CallableRunnable类似,同样是只有一个抽象方法的函数式接口。不同的是,Callable提供的方法是有返回值的,而且支持泛型

1
2
3
4
@FunctionalInterface
public interface Callable<V> {
V call() throws Exception;
}

那么这个返回值该如何接受呢?这就引出了实现Callable 接口的几种方法了,可以使用下面的Callable+ Future 接口完成,也可以使用再下面的Callable+ FutureTask 类进行实现,一般来说,选择后者的做法居多。

其实这两者的差别就是使用:submit(Runnable task)submit(Callable task) 的差别而已了!


2.3.2 Future接口

Future接口只有几个比较简单的方法:

1
2
3
4
5
6
7
8
public abstract interface Future<V> {
public abstract boolean cancel(boolean paramBoolean);
public abstract boolean isCancelled();
public abstract boolean isDone();
public abstract V get() throws InterruptedException, ExecutionException;
public abstract V get(long paramLong, TimeUnit paramTimeUnit)
throws InterruptedException, ExecutionException, TimeoutException;
}

cancel方法是试图取消一个线程的执行,但是并不一定能够成功,因为任务可能已完成、已取消、或者一些其它因素不能取消,存在取消失败的可能。boolean类型的返回值是“是否取消成功”的意思。参数paramBoolean表示是否采用中断的方式取消线程执行。

所以有时候,为了让任务有能够取消的功能,就使用Callable来代替Runnable。如果为了可取消性而使用 Future但又不提供可用的结果,则可以声明 Future形式类型、并返回 null作为底层任务的结果。

那一般是怎么配合使用Callable的呢?Callable一般是配合线程池工具ExecutorService来使用的。

ExecutorService可以使用submit方法来让一个Callable接口执行。它会返回一个Future,我们后续的程序可以通过这个Futureget方法得到结果。

再注意些:下面是通过new Task ();然后将这个Task使用线程池的submit进行提交的,submit是有返回值的,然后使用一个Future<>进行接收,再在下面我讲到了FutureTask之后,发现我们使用FutureTask或者更加方便一些,应该将这两者结合起来,就能更明白FutureTask的作用了。

简单的使用Future 接口的Demo如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 自定义Callable
class Task implements Callable<Integer>{
// 因为是接口,且只有一个call,所以实现他即可
@Override
public Integer call() throws Exception {
// 模拟计算需要一秒
Thread.sleep(1000);
return 2;
}

public static void main(String args[]){
// 申请线程池
ExecutorService executor = Executors.newCachedThreadPool();
// 新建一个Task
Task task = new Task();
// 使用submit方法进行任务的提交,返回一个Future
Future<Integer> result = executor.submit(task);

// 注意调用get方法会阻塞当前线程,直到得到结果。
// 所以实际编码中建议使用可以设置超时时间的重载get方法。
System.out.println(result.get());
}
}

输出结果:

1
2

在线程池中的使用,可以具体看看我之前写的博客中的一个Demo:Callable+ThreadPoolExecutor,可以申请一个List,用来装返回的数据:List<Future<String>> futureList = new ArrayList<>();,然后可以对这个futureList进行遍历输出。


2.3.3 FutureTask类

关于FutureTask的源码分析,我在这篇文章进行了分析:Java 并发 - 多线程:FutureTask源码分析。可以查看一下。

需要注意通过使用Thread 方式配合FutureTask 的方式时,主线程在调用 futureTask.get() 方法时阻塞主线程;然后 Callable 内部开始执行,并返回运算结果;此时 futureTask.get() 得到结果,主线程恢复运行。当然,如果不希望阻塞主线程,可以考虑利用 ExecutorService,把 FutureTask 放到线程池去管理执行。

示例Demo1:

配合Executors 进行使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 自定义Callable,与上面一样
class Task implements Callable<Integer>{
@Override
public Integer call() throws Exception {
// 模拟计算需要一秒
Thread.sleep(1000);
return 2;
}
public static void main(String args[]){
// 申请线程池
ExecutorService executor = Executors.newCachedThreadPool();
// 注意,这里是申请了一个FutureTask,将实现Callable 接口的Task作为参数传入
FutureTask<Integer> futureTask = new FutureTask<>(new Task());
// 提交这个FutureTask,注意和最上面的Future实现的方式对比:submit没有返回值
executor.submit(futureTask);
// 使用的是FutureTask的一些方法
System.out.println(futureTask.get());
}
}

使用上与第一个Demo有一点小的区别。首先,调用submit方法是没有返回值的。这里实际上是调用的submit(Runnable task)方法,而上面的Demo,调用的是submit(Callable task)方法。

然后,这里是使用FutureTask直接取get取值,而上面的Demo是通过submit方法返回的Future去取值。

在很多高并发的环境下,有可能Callable 和FutureTask 会创建多次。FutureTask能够在高并发环境下确保任务只执行一次

示例Demo2:

配合线程Thread 进行使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    
public class FutureTaskExample implements Callable<Integer> {
public Integer call() {
return 123;
}

public static void main(String[] args) throws ExecutionException, InterruptedException {
// 创建实例
FutureTaskExample mc = new FutureTaskExample();
// 将实现Callable 接口作为参数给FutureTask
FutureTask<Integer> ft = new FutureTask<>(mc);
// 因为FutureTask 实现Runnable 接口,所以使用线程创建的方式
Thread thread = new Thread(ft);
thread.start();
System.out.println(ft.get());
}
}

输出:

1
123

还可以参考的Demo3 创建的方式其实一样,就是较为简单易看,这里作为一个参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
public class FutureTaskExample {

public static void main(String[] args) throws ExecutionException, InterruptedException{
// 创建一个FutreTask,传入一个callable 实现的函数
FutureTask<Integer> futureTask = new FutureTask<Integer> (new Callable<Integer>(){
// 里面进行一个计数,并到点返回
@Override
public Integer call() throws Exception {
int result = 0;
for (int i = 0; i < 100; i++){
Thread.sleep(10);
result += i;
}
return result;
}
});

// 使用futureTask 已经封装过的任务,创建一个线程
Thread computeThread = new Thread(futureTask);
computeThread.start();

// 实现其他线程
Thread otherThread = new Thread(() -> {
System.out.println("other task is running...");
try{
Thread.sleep(100);
} catch (InterruptedException e){
e.printStackTrace();
}
});

// 启动其他线程
otherThread.start();
System.out.println(futureTask.get());
}
}

输出:

1
2
other task is running...
4950

2.4 其他实现线程的方法

Java5 之后的ExecutorsExecutors工具类可以用来创建线程池。

Executors工具类是用来创建线程池的,这个线程池可以指定线程个数,也可以不指定,也可以指定定时器的线程池,它有如下常用的方法:

1
2
3
4
newFixedThreadPool(int nThreads):创建固定数量的线程池
newCachedThreadPool():创建缓存线程池
newSingleThreadExecutor():创建单个线程
newScheduledThreadPool(int corePoolSize):创建定时器线程池

这就涉及线程池的概念了,关于线程池的一些可以查看之前的文章:Java 并发 - 多线程:深入线程池原理


3.深入理解Thread类

3.1 Thread类构造方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
// Thread的构造方法
private Thread(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
// 如果名字为空则抛出异常,线程名字不可为空
if (name == null) {
throw new NullPointerException("name cannot be null");
}

// 名字
this.name = name;

// 设置线程的父线程为当前线程
Thread parent = currentThread();
SecurityManager security = System.getSecurityManager();

// 如果线程组为空
if (g == null) {
/* Determine if it's an applet or not */

// 如果安全管理security不为空,则使用安全管理security获取一个线程组
if (security != null) {
g = security.getThreadGroup();
}

// 如果没有安全管理security没有设置线程组,则使用父线程组
if (g == null) {
g = parent.getThreadGroup();
}
}

// 无论是否显式传入threadgroup,都可以进行checkAccess
g.checkAccess();

// 权限检查
if (security != null) {
if (isCCLOverridden(getClass())) {
security.checkPermission(
SecurityConstants.SUBCLASS_IMPLEMENTATION_PERMISSION);
}
}

// 这里会使用synchronized去同步线程组,然后进行一个nUnstartedThreads++;
g.addUnstarted();

this.group = g;
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
if (security == null || isCCLOverridden(parent.getClass()))
this.contextClassLoader = parent.getContextClassLoader();
else
this.contextClassLoader = parent.contextClassLoader;
// 初始化AccessControlContext类型的私有属性
this.inheritedAccessControlContext =
acc != null ? acc : AccessController.getContext();
this.target = target;
// 设置线程优先级
setPriority(priority);
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
// 两个对用于支持ThreadLocal的私有属性
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
// 保存指定的栈堆大小
this.stackSize = stackSize;

// 设置线程ID
this.tid = nextThreadID();
}

Thread类有很多构造方法,不过都是以上面这个构造方法为基准进行改造的,所以总的来说了解上面这个构造方法就可以了。比如有以下这么几个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 无参构造
public Thread() {
this(null, null, "Thread-" + nextThreadNum(), 0);
}

// 带Runnable的构造
public Thread(Runnable target) {
this(null, target, "Thread-" + nextThreadNum(), 0);
}

// 带ThreadGroup和Runnable的构造
public Thread(ThreadGroup group, Runnable target) {
this(group, target, "Thread-" + nextThreadNum(), 0);
}

// 带名字参数的构造
public Thread(String name) {
this(null, null, name, 0);
}

我们大多调用下面两个构造方法:

1
2
Thread(Runnable target)
Thread(Runnable target, String name)

Thread的构造方法主要有以下的几个参数:

  1. g:线程组,指定这个线程是在哪个线程组下;

  2. target:指定要执行的任务;

  3. name:线程的名字,多个线程的名字是可以重复的。如果不指定名字,见片段2;

  4. acc:用于初始化私有变量inheritedAccessControlContext

    它是一个私有变量,但是在Thread类里只有init方法对它进行初始化,在exit方法把它设为null。其它没有任何地方使用它。一般我们是不会使用它的,那什么时候会使用到这个变量呢?可以参考这个stackoverflow的问题:Restrict permissions to threads which execute third party software

  5. inheritThreadLocals:可继承的ThreadLocalThread类里面有两个私有属性来支持`ThreadLocal。


3.2 Thread类的几个常用方法

3.2.1 currentThread

currentThread():静态方法,返回对当前正在执行的线程对象的引用;

1
2
3
4
5
6
7
/**
* Returns a reference to the currently executing thread object.
*
* @return the currently executing thread.
*/
@HotSpotIntrinsicCandidate
public static native Thread currentThread();

像获取当前线程的名字,一般就可以这么使用了:Thread.currentThread().getName();

3.2.2 start

start()开始执行线程的方法,java虚拟机会调用线程内的run()方法;

需要注意的是:不可以多次启动线程,而且线程一旦完成执行,就不可以再次启动。意思就是同一个线程多次调用start()就会出现问题了!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 线程被执行,JVM调用run方法
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)// 状态校验 0:NEW 新建状态
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(); // 调用native方法执行线程run方法
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 */
}
}
}

private native void start0();

3.2.3 yield

yield()yield()指的是当前线程愿意让出对当前处理器的占用。这里需要注意的是,就算当前线程调用了yield()方法,程序在调度的时候,也还有可能继续运行这个线程的;

1
public static native void yield();

3.2.4 sleep

sleep():静态方法,使当前线程睡眠一段时间;

1
2
// 进行睡眠 线程不会失去任何监视器的所有权。
public static native void sleep(long millis) throws InterruptedException;

3.2.5 join

join():使当前线程等待另一个线程执行完毕之后再继续执行,内部调用的是Object类的wait方法实现的;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 最多等待millis毫秒,使此线程死亡。如果参数为0则意味着永远等待。
public final synchronized void join(final long millis)
throws InterruptedException {
if (millis > 0) {
// 测试此线程是否处于活动状态。如果线程已启动但尚未死亡,则它是活动的。
if (isAlive()) {
final long startTime = System.nanoTime();
// 等待多长时间
long delay = millis;
do {
wait(delay);
} while (isAlive() && (delay = millis -
TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - startTime)) > 0);
}
} else if (millis == 0) { // millis为0,则进行永远等待
while (isAlive()) {
wait(0);
}
} else {
throw new IllegalArgumentException("timeout value is negative");
}
}

4.关于submit与execute

关于execute 的详解请看这里:Java 并发 - 多线程:深入线程池原理,这里列出其源码,方便对比。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
else if (!addWorker(command, false))
reject(command);
}

submit方法则是在ExecutorService接口中定义的。

1
2
3
4
5
6
7
8
//ExecutorService
public` `interface` `ExecutorService ``extends` `Executor {
  ...
   Future submit(Callable task);
   Future submit(Runnable task, T result);
   Future submit(Runnable task);
  ...
}

在其子类AbstractExecutorService实现了submit方法。

submit有Future返回值 :

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//AbstractExecutorService
/**
* @throws RejectedExecutionException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
execute(ftask);
return ftask;
}

/**
* @throws RejectedExecutionException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public <T> Future<T> submit(Runnable task, T result) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task, result);
execute(ftask);
return ftask;
}

/**
* @throws RejectedExecutionException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}

在AbstractExecutorService实现的submit方法实际上是一个模板方法,定义了submit方法的算法骨架,其execute交给了子类。

尽管submit方法能提供线程执行的返回值,但只有实现了Callable才会有返回值,而实现Runnable的线程则是没有返回值的,也就是说在上面的3个方法中,submit(Callable<T> task)能获取到它的返回值,submit(Runnable task, T result)能通过传入的载体result间接获得线程的返回值或者准确来说交给线程处理一下,而最后一个方法submit(Runnable task)则是没有返回值的,就算获取它的返回值也是null。

从上面的源码可以看到,这三者方法几乎是一样的,关键就在于:

1
2
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);

它是如何将一个任务作为参数传递给了newTaskFor,然后调用execute方法,最后进而返回ftask的呢?

关于newTaskFor(task) 其源码为:

1
2
3
4
5
6
7
8
9
//AbstractExecutorService#newTaskFor
// 返回了一个FutureTask实例
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
return new FutureTask<T>(callable);
}

protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
return new FutureTask<T>(runnable, value);
}

上面我们已经说了,FutureTask实现了Future和Runnable接口。Future接口是Java线程Future模式的实现,可用用来异步计算,实现Runnable接口表示可以作为一个线程执行。FutureTask实现了这两个接口意味着它代表异步计算的结果,同时可以作为一个线程交给Executor来执行。

两者的主要区别,简略说明有以下几个:

  1. executor 方法,无返回值;submit 方法,可以提供Future < T > 类型的返回值。

  2. excute 方法会抛出异常;sumbit 方法不会抛出异常,除非你调用Future.get()

  3. excute 入参Runnable;submit 入参可以为Callable,也可以为Runnable。

注:关于第二种,传入Runnable 和 result 的使用一直有疑惑,这里给出一个Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Submit2 {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executor = Executors.newSingleThreadExecutor();
Data data = new Data();
Future<Data> future = executor.submit(new Task(data), data);
System.out.println(future.get().getName());
}
}
// 这里就是充当result了
class Data {
String name;
public String getName() { return name;}
public void setName(String name) { this.name = name;}
}

// 新建一个Task 继承Runnable接口
class Task implements Runnable {
Data data;
public Task(Data data) { this.data = data; }

public void run() {
System.out.println("This is ThreadPoolExetor#submit(Runnable task, T result) method.");
data.setName("kevin");
}
}

5.几种比较

5.1 实现接口 VS 继承Thread

这里再稍微总结一下线程创建的两种方式:

  1. 继承Thread类,并重写run()方法
  2. 实现Runnable接口,覆盖接口中的run()方法,并把Runnable接口的实现扔给Thread。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
// 第一种
MyThread myThread = new MyThread();
myThread.start();

// 第二种
new Thread(() -> System.out.println("自己实现的run-2")).start();
}

// 第一种继承Thread方式
public static class MyThread extends Thread {
@Override
public void run() {
System.out.println("自己实现的run-1");
}
}

这里抛出一个问题:写的两种创建线程的方式,都涉及到了run()方法,那么Thread里的run()方法具体是怎么实现的吗?

Thread 中的run()方法里东西很少,就一个 if 判断:

1
2
3
4
5
6
@Override
public void run() {
if (target != null) {
target.run();
}
}

有个target对象,判断该变量是否为null,非空的时候,去执行target对象中的run()方法,否则啥也不干。而这个target对象,就是我们说的Runnable

1
2
/* What will be run. */
private Runnable target;

Runnable类很简单,就一个抽象方法:

1
2
3
4
5
@FunctionalInterface
public interface Runnable {
// run方法是抽象的!
public abstract void run();
}

这个抽象方法也是run()!如果我们使用Runnable,就需要实现这个方法,由于这个Runnable类上面标了@FunctionalInterface注解,所以可以使用函数式编程。

这样一来:

  1. 假如我用第一种方式:继承了Thread类,然后重写了run()方法,那么它就不会去执行上面这个默认的run()方法了(即不会去判断target),会执行我重写的run()方法逻辑。
  2. 假如我是用的第二种方式:实现Runnable接口的方式,那么它会执行默认的run()方法,然后判断target不为空,再去执行我在Runnable接口中实现的run()方法。

还有个问题:如果我既继承了Thread类,同时我又实现了Runnable接口,比如这样,最后会打印什么信息出来呢?

1
2
3
4
5
6
7
8
9
10
public static void main(String[] args) {
new Thread(() -> System.out.println("runnable run")) // 实现Runnable接口的写法
{
// 继承Thread的写法
@Override
public void run() {
System.out.println("Thread run");
}
}.start();
}

输出:

1
Thread run

其实这个答案很简单,我们来分析一下代码便知:其实是 new 了一个对象(子对象)继承了Thread对象(父对象),在子对象里重写了父类的run()方法;然后父对象里面扔了个Runnable进去,父对象中的run()方法就是最初那个带有 if 判断的run()方法。

现在执行start()后,肯定先在子类中找run()方法,找到了,父类的run()方法自然就被干掉了,所以会打印出:Thread run。

如果我们现在假设子类中没有重写run()方法,那么必然要去父类找run()方法,父类的run()方法中就得判断是否有Runnable传进来,现在有一个,所以执行Runnable中的run()方法,那么就会打印:Runnable run 出来。

说白了,就是 Java 语言本身的父子继承关系,会优先执行子类重写的方法而已!

我理解的实现接口会更好一些,因为:

  1. Java 不支持多重继承,因此继承了 Thread 类就无法继承其它类,但是可以实现多个接口;
  2. 类可能只要求可执行就行,继承整个 Thread 类开销过大,如果使用线程时不需要使用Thread类的诸多方法,显然使用Runnable 接口更为轻量。
  3. Runnable 接口出现更符合面向对象,将线程单独进行对象的封装。
  4. Runnable 接口出现,降低了线程对象和线程任务的耦合性。

所以总的来说,还是优先使用实现Runnable 接口方式进行线程的实现较为友好。


5.2 Runnable接口 VS Callable接口

总得来说有两个不一样的地方:

  1. Runnable 接口不会返回结果或抛出检查异常,但是Callable 接口可以
  2. Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。
  3. 如果想任务有能够取消的功能,就使用Callable来代替Runnable

Runnable自Java 1.0以来一直存在,但Callable仅在Java 1.5中引入,目的就是为了来处理Runnable不支持的用例。Runnable 接口不会返回结果或抛出检查异常,但是Callable 接口可以。所以,如果任务不需要返回结果或抛出异常推荐使用 Runnable 接口,这样代码看起来会更加简洁。

Runnable接口中的run()方法的返回值是void,它做的事情只是纯粹地去执行run()方法中的代码而已;

1
2
3
4
5
@FunctionalInterface
public interface Runnable {
// 被线程执行,没有返回值也无法抛出异常
public abstract void run();
}

Callable接口中的call()方法是有返回值的,是一个泛型,和Future、FutureTask配合可以用来获取异步执行的结果。

这其实是很有用的一个特性,因为多线程相比单线程更难、更复杂的一个重要原因就是因为多线程充满着未知性,某条线程是否执行了?某条线程执行了多久?某条线程执行的时候我们期望的数据是否已经赋值完毕?无法得知,我们能做的只是等待这条多线程的任务执行完毕而已。而Callable+Future/FutureTask却可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务,真的是非常有用。

1
2
3
4
5
6
7
8
9
@FunctionalInterface
public interface Callable<V> {
/**
* 计算结果,或者在无法这样做的时候抛出异常
* @return 计算得出的结果
* @throws 如果无法计算结果,则抛出异常
*/
V call() throws Exception;
}

注意:

  • 工具类 Executors 可以实现 Runnable 对象和 Callable 对象之间的相互转换。(Executors.callable(Runnable task)Executors.callable(Runnable task,Object resule))。

6.小结

自上面整个流程分析下来,可以很清楚的对几种创建线程方式的理解了,总的来说还是实现Runnable接口比继承Thread更好一些,所以对于我来说比较偏向于使用继承接口,在继承接口有两种可供选择,一种是有返回值的callable,一种是没有返回值的Runnable接口,这就要具体情况具体分析了,看看实际过程中需要使用哪种,对于Thread类也更清晰了,Thread类的几个常用方法也有了一定的理解,总得来说学习过程也更清晰了,还是很开心的。

以上参考

  1. JAVA中ThreadPoolExecutor线程池的submit方法详解
  2. 线程池的submit和execute方法区别
  3. 书籍:深入浅出Java多线程
  4. 书籍:Java并发编程的艺术