这一系列文章开始讲解java多线程相关知识,下面这篇文章简单地介绍下新建线程的3种方式。
1 浅谈进程和线程
1.1 进程
进程是正在运行的程序的实例,是系统进行
资源分配的基本单位
。
进程的3个基本状态
就绪(Ready)
当进程已分配到除CPU以外的所有必要的资源,只要获得CPU便可立即执行。
运行(Running)
当进程已获得CPU,其程序正在CPU上执行。
阻塞(Blocked)
正在执行的进程,由于等待某个事件发生而无法执行时,便放弃CPU而处于阻塞状态。引起进程阻塞的事件可有多种,例如,等待I/O完成、申请缓冲区不能满足、等待信件(信号)等。
1.2 线程
线程是操作系统能够进行
任务调度和执行的最小单位
。它被包含在进程之中,是进程中的实际运作单位。
一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。
线程5种状态
新建(new Thread)
当创建Thread类( new Thread() )的一个实例时,在调用start方法之前的处于新建状态,它还没有分配到系统资源,这时只能启动或终止它,任何其他操作都会引发异常。
就绪(runnable)
执行 thread.start() 方法后,操作系统分配该线程除CPU之外所必须的资源,等待CPU调度线程即可执行。
运行(running)
线程获得CPU资源(获得CPU调度)执行任务。
阻塞(Blocked)
如果一个线程在执行过程中需要等待某个事件发生,则让出CPU暂停运行,即进入堵塞状态。
死亡(dead)
当run() 方法返回,或别的线程调用stop()方法,线程进入死亡态。
线程状态转换图如下:
1.3 进程和线程的关系
2 Thread类创建API
方法 | 描述 |
---|---|
Thread() | 构造器,线程名称:“Thread-”+线程序号 |
Thread(String name) | 带name的构造器,name: 新线程的名称 |
Thread(Runnable target) | 带Runnable的构造器, Runnable: 线程被启动后被CPU调度时,会执行Runnable实例的run()方法 |
Thread(Runnable target, String name) | 带Runnable和name的构造器, Runnable: 线程被启动后被CPU调度时,会执行Runnable实例的run()方法, name: 新线程的名称 |
synchronized void start() | 启动此线程,JVM将会执行这个线程的 run() 方法 |
3 创建线程方式一:extends Thread
定义一个类 extends Thread,并重写 run() 方法,run()方法的方法体就子线程的任务逻辑.。
/**
* Description: 自定义线程类, extends Thread
*
* @author Xander
* datetime: 2020/9/16 19:22
*/
public class XdThread extends Thread {
public XdThread() {
}
/**
* 指定线程名称
* @param name 线程名称
*/
public XdThread(String name) {
super(name);
}
/**
* run()方法是子线程执行的任务逻辑
*/
@Override
public void run() {
System.out.println("子线程名称:" + Thread.currentThread().getName() + " 开始执行任务...");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("子线程名称:" + Thread.currentThread().getName() + " 任务结束...");
}
}
/**
* 创建线程方式一:定义一个类 extends Thread,并重写 run() 方法,run()方法的方法体就子线程的任务逻辑
*/
@Test
public void testNewThread() throws InterruptedException {
//新建线程,不指定线程名称,系统会给线程定义一个名称
XdThread thread = new XdThread();
//启动子线程
thread.start();
//新建线程,指定线程名称
XdThread threadWithName = new XdThread("Another-Thread");
//启动子线程
threadWithName.start();
TimeUnit.SECONDS.sleep(3);
}
运行结果:
子线程名称:Thread-0 开始执行任务...
子线程名称:Another-Thread 开始执行任务...
子线程名称:Thread-0 任务结束...
子线程名称:Another-Thread 任务结束...
4 创建线程方式二:Runnable
Runnable源码
Runnable 可以用 @FunctionalInterface 标识,是一个函数式接口,我们可以通过 lambda 表达式创建一个 Runnable 的实例。
@FunctionalInterface
public interface Runnable {
void run();
}
/**
* Description: 通过实现 Runnable 接口创建线程
*
* @author Xander
* datetime: 2020/9/16 19:34
*/
public class XdRunnable implements Runnable {
/**
* 线程任务执行逻辑
*/
@Override
public void run() {
System.out.println("Runnable方式,子线程名称:" + Thread.currentThread().getName() + " 开始执行任务...");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Runnable方式,子线程名称:" + Thread.currentThread().getName() + " 任务结束...");
}
}
/**
* 创建线程方式二:实现Runnable接口
*/
@Test
public void testRunnable() throws InterruptedException {
Thread thread = new Thread(new XdRunnable());
//启动子线程
thread.start();
// 通过 Lambda 创建Runnable实例
new Thread(() -> {
System.out.println("我是另一个子线程,开始执行任务...");
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("我是另一个子线程,执行结束...");
}).start();
TimeUnit.SECONDS.sleep(3);
}
运行结果:
Runnable方式,子线程名称:Thread-0 开始执行任务...
我是另一个子线程,开始执行任务...
我是另一个子线程,执行结束...
Runnable方式,子线程名称:Thread-0 任务结束...
5 创建线程方式三:Callable + FutureTask
上面两种方式创建线程是没有返回值
,也不能抛出 checked Exception
。
如果我们的业务场景需要接收返回值
,或者当子线程执行异常时,我们需要catch子线程抛出的异常
,这时,我们就可以用到 Callable
+ FutrueTask
新建线程。
5.1 Runnable、Future、RunnableFuture、FutureTask 和 Callable 之间的关系
从下面的类图中,我们可以看出,RunnableFuture
接口继承了 Runnable
接口和 Future
接口,而 FutureTask
类实现了 RunnableFuture
接口,同时 Callable
是FutureTask
类的一个成员变量。
5.2 Runnable、Future、RunnableFuture、FutureTask 和 Callable 的源码
5.2.1 Callable
定义 call() 方法计算结果,或在无法正常计算时抛出异常。泛型V
指定返回结果值的类型。
@FunctionalInterface
public interface Callable<V> {
/**
* 计算结果,或在无法正常计算时抛出异常。
*/
V call() throws Exception;
}
5.2.2 Runnable
当线程被cpu执行时,会执行run()方法。
@FunctionalInterface
public interface Runnable {
void run();
}
5.2.3 Future
Future定义了几个操作方法,用于操作 Callable 的 call() 方法的执行任务。
public interface Future<V> {
/**
* 取消任务执行
*/
boolean cancel(boolean mayInterruptIfRunning);
/**
* 判断任务是否是在完成之前被取消了
*/
boolean isCancelled();
/**
* 如果任务执行结束,返回true
*/
boolean isDone();
/**
* 等待任务完成,获取结果
*/
V get() throws InterruptedException, ExecutionException;
/**
* 在规定时间内完成了任务,那么就会正常获取到返回值;而如在指定时间内没有计算出结果,那么就会抛出 TimeoutException
*/
V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
5.2.4 RunnableFuture
继承 Runnable, Future接口。
public interface RunnableFuture<V> extends Runnable, Future<V> {
/**
* Sets this Future to the result of its computation
* unless it has been cancelled.
*/
void run();
}
5.2.5 FutureTask
实现 RunnableFuture,因此FutureTask也是Runnable和Future接口的实现类,也就是实现了 Runnable 和 Future 接口中的抽象方法。
也就是说 FutureTask 可以作为 Runnable ,通过Thread(Runnable target)
或者Thread(Runnable target, String name)
方式新建线程,当线程被CPU执行时,会调用 FutureTask 的 run()方法。从下面源码中,我们看到run()方法中会执行 Callable 实例的 call() 方法。
public class FutureTask<V> implements RunnableFuture<V> {
...
//成员变量Callable
private Callable<V> callable;
...
// 构造器,入参是一个 Callable 实例
public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW; // ensure visibility of callable
}
...
// FutureTask也是一个Runnable接口的实现类,线程被CPU执行时,会调用run()方法,run()方法中会执行 Callable 实例的 call() 方法。
public void run() {
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
try {
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
// 执行 Callable 实例的 call() 方法,并保存结果
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex);
}
if (ran)
set(result);
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
...
}
5.2.6 小结
我们可以用 Future.get() 来获取 Callable 接口返回的执行结果,还可以通过 Future.isCancelled() 判断任务是否被取消了等。
在call() 未执行完毕之前,调用get()的线程(假定此时是主线程)会被阻塞,直到 call() 方法返回了结果后,此时future.get() 才会得到该结果,然后主线程才会切换到 runnable(就绪) 状态,申请cpu调度。
所以 Future 是一个 存储器,它存储了 call() 这个任务的结果,而这个任务的执行时间是无法提前确定的,因为这完全取决于 call() 方法执行的情况。
Future接口API
方法 | 描述 |
---|---|
boolean cancel(boolean mayInterruptIfRunning) | 取消任务执行 |
boolean isCancelled() | 判断任务是否是在完成之前被取消了 |
boolean isDone() | 如果任务执行结束,返回true |
V get() throws InterruptedException, ExecutionException | 等待任务完成,获取结果 - 1. 任务正常完成:get方法会立刻返回结果 - 2. 任务 尚未完成 (任务还没开始或者进行中):get将阻塞 并直到任务完成。- 3. 任务执行过程中抛出 Exception:get 方法会抛出 ExecutionException:这里的抛出异常,是Call()执行时产生的那个异常,看到这个异常类型是 java.util.concurrent.ExecutionException。不论 call() 执行时抛出的异常类型是什么,最后get方法抛出的异常类型都是 ExecutionException。 - 4. 任务被 取消 :get方法会抛出 CancellationException- 5. 任务 超时 ,get方法有一个重载方法,是传入一个延迟时间的,如果时间到了还没有获得结果,get方法就会抛出TimeoutException。 |
V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException; | 在规定时间内完成了任务,那么就会正常获取到返回值;而如在指定时间内没有计算出结果,那么就会抛出 TimeoutException,超时不获取,任务需取消 |
5.3 Callable + FutureTask 创建线程,并获取结果
因为 FutureTask是 Runnable 和 Future 接口的实现类。
/**
* Description: 通过实现 Callable 接口创建线程,允许有返回值
*
* @author Xander
* datetime: 2020/9/16 19:52
*/
public class XdCallable implements Callable<Integer> {
/**
* 线程任务执行逻辑
*
* @return
* @throws Exception
*/
@Override
public Integer call() throws Exception {
System.out.println("Callable方式,子线程名称:" + Thread.currentThread().getName() + " 开始执行任务...");
// 返回一个 0-100(不包含) 随机整数
Random random = new Random();
int num = random.nextInt(100);
TimeUnit.SECONDS.sleep(2);
System.out.println("Callable方式,子线程名称:" + Thread.currentThread().getName() + " 任务结束...");
return num;
}
}
/**
* 创建线程方式三:实现 Callable 接口
*/
@Test
public void testCallable() throws ExecutionException, InterruptedException {
// 通过子线程返回一个 0-100(不包含) 随机整数
FutureTask future = new FutureTask(new XdCallable());
// FutureTask 可以作为 Runnable ,通过 Thread(Runnable target) 新建线程
Thread thread = new Thread(future);
//启动子线程
thread.start();
System.out.println("子线程返回:" + future.get());
System.out.println("主线程结束");
}
运行结果:
Callable方式,子线程名称:Thread-0 开始执行任务...
Callable方式,子线程名称:Thread-0 任务结束...
子线程返回:40
主线程结束
6 结语
- 方式一:extends Thread,实现简单,但是由于类单一继承原则,这种方式下线程类已经继承Thread类了,就不能再继承其他类;
- 方式二:实现 Runnable 接口方式,不能返回一个返回值,也不能抛出 checked Exception。
- 方式三:实现 callable 接口,允许子线程有返回值,能够抛出 checked Exception。
extends Thread 方式限制了类扩展,所以在工作中用得较少;
如果子线程没有返回值,也不需要在调用线程中捕获子线程抛出的异常,则可以使用 Runnable 方式新建线程;
如果需要子线程有返回值或者能够抛出 checked Exception,则可以使用 Callable + FutureTask 方式新建线程。