一、多线程的作用
1️⃣发挥多核CPU的优势
随着工业的进步,现在的笔记本、台式机乃至商用的应用服务器至少也都是双核的,4核、8核甚至 16 核的也不少见。如果是单线程的程序,那么在双核 CPU 上就浪费了50%,在 4 核 CPU 上就浪费了 75%。单核 CPU 上所谓的“多线程”是假的多线程,同一时间处理器只会处理一段逻辑,只不过线程之间切换得比较快,看着像多个线程“同时”运行罢了。多核 CPU 上的多线程才是真正的多线程,它能让多段逻辑同时工作。多线程,可以真正发挥出多核 CPU 的优势来,达到充分利用 CPU 的目的。
2️⃣防止阻塞
从程序运行效率的角度来看,单核 CPU 不但不会发挥出多线程的优势,反而会因为在单核 CPU 上运行多线程导致线程上下文的切换(多线程的上下文切换是指 CPU 控制权由一个正在运行的线程切换到另外一个就绪并等待获取 CPU 执行权的线程的过程
),而降低程序整体的效率。但是单核 CPU 还是要应用多线程,就是为了防止阻塞。试想,如果单核 CPU 使用单线程,那么只要这个线程阻塞了,比方说远程读取某个数据,对方迟迟未返回又没有设置超时时间,那么整个程序在数据返回来之前就停止运行了。多线程可以防止这个问题,多条线程同时运行,哪怕一条线程的代码执行读取数据阻塞,也不会影响其它任务的执行。
3️⃣便于建模
这是另外一个不明显的优点。假设有一个大的任务 A,单线程编程,那么就要考虑很多,建立整个程序模型比较麻烦。但是如果把这个大的任务分解成几个小任务:任务 B、任务 C 和任务 D,分别建立程序模型,并通过多线程分别运行这几个任务,那就简单很多了。
二、Java创建线程的四种方式
Java 使用 Thread 类代表线程,所有的线程对象必须是 Thread 类或其子类的实例。Java 可以用如下四种方式来创建线程:
①继承 Thread 类创建线程没有返回值
;
②实现 Runnable 接口创建线程没有返回值
;
③实现 Callable 接口,通过 FutureTask 包装器来创建 Thread 线程有返回值
;
④线程池:使用ExecutorService、Callable、Future 实现有返回结果的线程有返回值
。
1️⃣------------------------继承Thread类创建线程---------------------
- 定义 Thread 类的子类,并重写该类的run(),该方法的方法体就是线程需要完成的任务,run() 也称为线程的执行体。
- 创建 Thread 子类的实例,也就是创建了线程对象。
- 启动线程,即调用线程的 start()。
代码实例:
public class MyThread extends Thread{//继承Thread类
public void run(){
//重写run方法
}
}
public class Main {
public static void main(String[] args){
new MyThread().start();//创建并启动线程
}
}
2️⃣------------------------实现Runnable接口创建线程---------------------
- 定义 Runnable 接口的实现类,同样要重写 run()。这个 run() 和 Thread 中的 run() 一样是线程的执行体。
- 创建 Runnable 实现类的实例,并用这个实例作为 Thread 的 target 来创建 Thread 对象,这个 Thread 对象才是真正的线程对象。
- 依然是通过调用线程对象的 start() 来启动线程。
public class MyThread implements Runnable {//实现Runnable接口
public void run(){
//重写run方法
}
}
public class Main {
public static void main(String[] args){
//创建并启动线程
MyThread myThread=new MyThread();
Thread thread=new Thread(myThread);
thread().start();
//或者new Thread(new MyThread()).start();
}
}
3️⃣------------------------使用Callable和Future创建线程---------------------
不同于 Runnable 接口,Callable 接口提供了一个 call() 为线程的执行体,call() 比 run() 功能要强大:
- call() 可以有返回值;
- call() 可以声明抛出异常。
Java5提供了 Future 接口来代表 Callable 接口里 call() 的返回值,并且为 Future 接口提供了一个实现类 FutureTask,这个实现类既实现了 Future 接口,还实现了 Runnable 接口,因此可以作为 Thread 类的 target。在 Future 接口里定义了几个公共方法来控制它关联的 Callable 任务:
boolean cancel(boolean mayInterruptIfRunning)
:试图取消该 Future 里面关联的 Callable 任务。get()
:返回 Callable 里 call() 的返回值,调用这个方法会导致程序阻塞,必须等到子线程结束后才会得到返回值。get(long timeout,TimeUnit unit)
:返回 Callable 里 call() 的返回值,最多阻塞 timeout 时间,经过指定时间没有返回抛出 TimeoutException。boolean isDone()
:若 Callable 任务完成,返回 true。boolean isCancelled()
:如果在 Callable 任务正常完成前被取消,返回 true。
创建并启动有返回值的线程的步骤如下:
- 创建 Callable 接口的实现类,并实现 call(),然后创建该实现类的实例(可以用 Java8 的Lambda 表达式创建 Callable 对象)。
- 使用 FutureTask 类来包装 Callable 对象及 call() 的返回值。
- 使用 FutureTask 对象作为 Thread 对象的 target 创建并启动线程(因为 FutureTask 实现了 Runnable 接口)。
- 调用 FutureTask 对象的 get() 来获得子线程执行结束后的返回值。
public class Main {
public static void main(String[] args){
//使用Lambda表达式创建Callable对象
//使用FutureTask类来包装Callable对象
FutureTask<Integer> task = new FutureTask<Integer>(
(Callable<Integer>)()->{
return 5;
}
);
new Thread(task,"有返回值的线程").start();
//实质上还是以Callable对象来创建并启动线程
try{
System.out.println("子线程的返回值:"+task.get());
//get()方法会阻塞,直到子线程执行结束才返回
}catch(Exception e){
ex.printStackTrace();
}
}
}
4️⃣----------使用ExecutorService、Callable、Future实现有返回结果的线程--------
ExecutorService、Callable、Future 三个接口实际上都是属于Executor 框架。返回结果的线程是在 JDK1.5 中引入的新特征,有了这种特征就不需要再为了得到返回值而大费周折了。而且自己实现了也可能漏洞百出。有返回值的任务必须实现 Callable 接口。类似的,无返回值的任务必须实现 Runnable 接口。
执行 Callable 任务后,可以获取一个 Future 的对象,在该对象上调用 get 就可以获取到 Callable 任务返回的Object。
注意:get()是阻塞的,线程无返回结果,get()会一直等待。
再结合线程池接口 ExecutorService 就可以实现传说中有返回结果的多线程了。如下是一个完整的有返回结果的多线程例子:
import java.util.concurrent.*;
import java.util.List;
import java.util.ArrayList;
//有返回值的线程
@SuppressWarnings("unchecked")
public class Test {
public static void main(String[] args) throws ExecutionException,
InterruptedException {
System.out.println("----程序开始运行----");
long start = System.currentTimeMillis();
int taskSize = 5;
// 创建一个线程池
ExecutorService pool = Executors.newFixedThreadPool(taskSize);
// 创建多个有返回值的任务
List<Future> list = new ArrayList<Future>();
for (int i = 0; i < taskSize; i++) {
Callable c = new MyCallable(i + " ");
// 执行任务并获取Future对象
Future f = pool.submit(c);
list.add(f);
}
// 关闭线程池
pool.shutdown();
// 获取所有并发任务的运行结果
for (Future f : list) {
// 从Future对象上获取任务的返回值,并输出到控制台
System.out.println(">>>" + f.get().toString());
}
long end = System.currentTimeMillis();
System.out.println("----程序结束运行----,程序运行时间【" + (end - start) + "毫秒】");
}
}
class MyCallable implements Callable<Object> {
private String taskNum;
MyCallable(String taskNum) {
this.taskNum = taskNum;
}
@Override
public Object call() throws Exception {
System.out.println(">>>" + taskNum + "任务启动");
long start = System.currentTimeMillis();
Thread.sleep(1000);
long end = System.currentTimeMillis();
System.out.println(">>>" + taskNum + "任务终止");
return taskNum + "任务返回运行结果,当前任务时间【" + (end - start) + "毫秒】";
}
}
三、对比
Runnable 接口中的 run() 的返回值是 void,它做的事情只是纯粹地去执行 run() 中的代码而已;Callable 接口中的 call() 是有返回值的,是一个泛型,和 Future、FutureTask 配合可以用来获取异步执行的结果。
这其实是很有用的一个特性,因为多线程相比单线程更难、更复杂的一个重要原因就是因为多线程充满着未知性,某条线程是否执行了,某条线程执行了多久,某条线程执行时期望的数据是否已经赋值完毕。无法得知,能做的只是等待这条多线程的任务执行完毕而已。而 Callable+Future/FutureTask 却可以获取多线程运行的结果,可以在等待时间太长没获取到需要的数据的情况下取消该线程的任务。
实现 Runnable 接口和实现 Callable 接口的方式基本相同。可以把这两种方式归为一种,这种方式与继承 Thread 类的方法之间的差别如下:
- 线程只是实现 Runnable 接口或实现 Callable 接口,还可以继承其他类。
- 这种方式下,多个线程可以共享一个 target 对象,非常适合多线程处理同一份资源的情形。
- 编程稍微复杂。如果需要访问当前线程,必须调用 Thread.currentThread()。
- 继承 Thread 类的线程类不能再继承其他父类( Java 单继承)。
注:一般推荐采用实现接口的方式来创建多线程。
四、线程的生命周期
Java 使用 Thread 类及其子类的对象来表示线程,在它的一个完整的生命周期中通常要经历如下的五种状态:
- 【新建】当一个 Thread 类或其子类的对象被声明并创建时,并没有调用该对象的 start(),新生的线程对象处于新建状态。
- 【就绪】当调用了线程对象的 start() 之后,将进入线程队列等待 CPU 时间片,此时它已具备了运行的条件。因为线程调度程序还没有把该线程设置为当前线程,所以此时该线程处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪状态。
- 【运行】当就绪的线程被调度并获得处理器资源时,便进入运行状态,开始执行 run() 当中的代码。
- 【阻塞】线程正在运行的时候,在某种特殊情况下,被人为挂起或执行输入输出操作时,让出 CPU 并临时中止自己的执行,进入阻塞状态。通常是为了等待某个事件的发生(比如说某项资源就绪)之后再继续运行。sleep 和 wait等方法都可以导致线程阻塞。
注意:
- Thread.sleep() 让线程从 running—>阻塞态。时间结束/interrupt—>runnable
- Object.wait() 让线程从 running—>等待队列。notify—>锁池—>runnable
- 【死亡】如果一个线程的 run() 执行结束或者调用 stop() 后,该线程就会死亡。对于已经死亡的线程,无法再使用 start() 令其进入就绪。