说到 Java 多线程的基础,你最先想到什么?是不是 Thread
?
其实具体来说,Java 实现多线程,主要有三种方法:
- 实现 Runnable 接口
- 继承 Thread 类
- 使用 Callable、Future 等有返回值的类
前两种方法没有返回值,但是是最常用的方法。
从 JDK 1.5 开始,上述的基本工作单元就和 Executor 框架分开了。
Runnable 接口
Runnable 是开启一个新线程的基础。我们所常用的“开启一个新线程”,或创建 Thread
对象,实质上就是继承 Runnable 接口。
如果某个类需要实现一个 Thread
类,但是已经继承了另一个类的话,就无法再直接继承 Thread 类了。
此时只能通过实现 Runnable 接口来满足需求,启动时将其实例作为参数传入 Thread 构造函数中:
1 | package java.lang; |
1 | // 实现 |
Thread
本质上是实现 Runnable。通过继承 Thread 并调用 t.start() 方法来启动线程。
t.start() 是 native 方法。
继承 Thread 类之后,我们需要覆写 run
方法来自定义操作:1
2
3
4
5
6
7class MyThread extends Thread {
public void run() {
doSomethingAsync();
}
}
1 | import java.lang.Thread; |
Thread 初始化的时候,会将 daemon 和 priority 设置为父线程的对应属性,再将父线程的 inheritableThreadLocal 复制过来。
如果直接调用 run 方法只会执行同一个线程的任务。
Thread 调用到最后一行,或者调用过程中出现了不可捕获的异常,线程会终止。
Thread 的 run 方法并不能抛出任何被检测到的异常,因此我们在设计的时候,需要在线程死亡之前,将异常传递到一个用于未被捕获的异常的处理器,该处理器实现 Thread.UncaughtExceptionHandler
类获取异常:
1 | import java.lang.Thread; |
1 | // UncaughtExceptionHandler 接口: |
通过扩展 Thread 类实现多线程的方法已不再被推荐:
- 首先,设计程序的时候应该从运行机制上去减少需要运行的任务数量
- 再者如有很多任务,为每个任务创建独立的线程,会造成很大的开销
我们应该引入“池”的概念,即使用线程池(thread pool)去解决问题。
Callable & Future
Callable 和 Future 都是接口类。
1 | package java.util.concurrent; |
1 | package java.util.concurrent; |
FutureTask
FutureTask
是 Future 接口的实现类,既可以提交到线程池中执行,也可以通过 run()
直接执行。
三种状态:未启动、已启动和已完成,比线程的粒度大很多,层次更高(不要跟线程状态混淆了)。
1 | package java.util.concurrent; |
调用 Future 接口方法时,FutureTask 对应不同状态时候的不同返回:
FutureTask 实现浅析:
可知 FutureTask 扩展了 Sync 继承了 AQS,其接口方法的实现是基于共享式获取资源而实现的。
再结合不同状态时候调用 Future 接口方法的不同返回,可知:
- FutureTask 还没完成的时候,如果调用 get(),“阻塞”的意思是调用 get() 的线程会进入同步队列中等待
- 直到 FutureTask 完成之后,AQS 的等待通知机制会通知同步队列头节点,唤醒第一个线程 get()
- 而调用 run() / cancel() 也会唤醒同步队列的第一个线程进行操作