前言
Java中创建线程的方式有四种:继承Thread类、实现Runnable接口、实现Callable接口、通过Executors工具类创建线程池等,作为多线程的基础来整理一下。
1、继承Thread类
说明:
Thread类本质上是实现了Runnable接口的一个实例。子类继承Thread类后需要重写run()方法,创建子类的实例后,通过start()方法启动线程。注意,Java只支持类的单继承,这种方式是不利于子类扩展的。
举例:
public class MyThread extends Thread {
Logger logger = LoggerFactory.getLogger(MyThread.class);
public void run() {
for (int i = 0; i < 5; i++) {
logger.info("线程" + Thread.currentThread().getName() + ":" + i);
}
}
}
@SpringBootTest
class MyThreadTest {
@Test
void testThread() {
MyThread myThread1 = new MyThread();
myThread1.start();
MyThread myThread2 = new MyThread();
myThread2.start();
MyThread myThread3 = new MyThread();
myThread3.start();
}
}
结果:从结果可知,启动线程的顺序是有序的,但是线程执行的顺序并非是有序的!!!
简化形式:
/** 使用线程Thread的匿名子类 **/
Thread thread1 = new Thread(){
public void run() {
for (int i = 0; i < 5; i++) {
logger.info(Thread.currentThread().getName() + ":" + i);
}
}
};
thread1.start();
/** 使用Lambda表达式创建线程Thread **/
Thread thread2= new Thread(()->{
for (int i = 0; i < 5; i++) {
logger.info(Thread.currentThread().getName() + ":" + i);
}
});
thread2.start();
2、实现Runnable接口
说明:
需要重写run()方法,并把实现类的实例作为参数传给Thread对象,通过start()方法启动该线程。
举例:
public class MyRunable implements Runnable{
Logger logger = LoggerFactory.getLogger(MyRunable.class);
@Override
public void run() {
try {
Thread.sleep(300);
} catch (InterruptedException e) {
e.printStackTrace();
}
logger.info("当前线程名称为:"+Thread.currentThread().getName());
}
}
@SpringBootTest
class MyThreadTest {
@Test
void testThread() {
MyRunable myRunable = new MyRunable();
Thread t1 = new Thread(myRunable,"t1线程");
Thread t2 = new Thread(myRunable,"t2线程");
Thread t3 = new Thread(myRunable,"t3线程");
t1.start();
t2.start();
t3.start();
}
}
结果:
3、实现Callable接口
说明:
Callable接口在java.util.concurrent包下,重写call()方法并有返回值,可抛异常,创建实现类的实例作为FutureTask类的包装对象,使用FutureTask对象作为参数创建Thread对象,通过start()方法启动线程,而且通过FutureTask对象的get()方法可获取子线程的返回值。
举例:
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;
public class MyCallable implements Callable<String> {
Logger logger = LoggerFactory.getLogger(MyCallable.class);
private int cnt = 10;
@Override
public String call() throws Exception {
for (int i = cnt; i > 0; i--) {
logger.info("商品当前剩余数量:" + cnt--);
}
return "商品售罄,欢迎下次抢购!";
}
}
@SpringBootTest
class MyThreadTest {
@Test
void testThread() throws Exception{
MyCallable myCallable = new MyCallable();
FutureTask<String> futureTask = new FutureTask<>(myCallable);
// 这三个线程谁获得了CPU执行权,谁就执行call()方法,直到返回结果
Thread th1 = new Thread(futureTask);
Thread th2 = new Thread(futureTask);
Thread th3 = new Thread(futureTask);
th1.start();
th2.start();
th3.start();
logger.info("询问-->是否完成商品售卖:" + futureTask.isDone());
// 阻塞其他的线程,保证执行完call()并返回结果值
logger.info(futureTask.get());
logger.info("再次询问-->是否完成商品售卖:" + futureTask.isDone());
}
}
结果:
最后总结一下,实现Callable接口与实现Runnable接口的区别:
- 实现Callable接口方式的call()方法可以抛异常,而Runnable的run()方法不会抛;
- 实现Callable接口方式在任务结束后能提供一个返回值,而Runnable没有;
- 使用Callable会拿到一个futureTask对象,它是异步计算的结果,提供了检查运算是否结束的方法(如isCancelled()、isDone()等)。而线程属于异步计算模型,是无法从其他线程得到call()方法的返回值的,这样可以使用futureTask对象来监控目标线程执行call()方法的情况,通过futureTask对象的get()方法可以阻塞当前线程,直到call()执行完毕返回结果。
4、通过Executors工具类创建线程池
1、为什么需要线程池?
每启动一个新线程都会有相应的性能开销,每个线程都需要给栈分配一些内存等,我们可以把并发执行的任务传递给一个线程池,来替代为每个并发执行的任务都启动一个新的线程。只要池里有空闲的线程,任务就会分配给一个线程执行。
合理利用线程池有以下三个优势:
- 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度:任务到达时,任务可以不需要的等到线程创建就能立即执行。
- 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。
然而,要合理的使用线程池,了解其原理是很重很要的!
2、线程池的运行流程
当提交一个新任务到线程池时,线程池的处理流程如图:
- 首先线程池判断基本线程池是否已满?没满,创建一个工作线程来执行任务。满了,则进入下个流程。
- 其次线程池判断工作队列是否已满?没满,则将新提交的任务存储在工作队列里。满了,则进入下个流程。
- 最后线程池判断整个线程池是否已满?没满,则创建一个新的工作线程来执行任务,满了,则交给拒绝策略来处理这个任务。
3、线程池的类型
Java 5 在 java.util.concurrent 包中自带了内置的线程池,如Executors工具类,它提供了多种类型的线程池,下面看源码里怎么定义这些类型的,同时需要注意以下几点:
ThreadPoolExecutor的参数说明:
- corePoolSize:线程池核心线程数量;
- maximumPoolSize:线程池能容纳的最大线程数量;
- keepAliveTime:线程保持活性的时间;
- unit:时间单位;
- workQueue:工作队列,是一种阻塞队列BlockingQueue。
线程池有以下六种类型:
- newFixedThreadPool、newSingleThreadExecutor、newCachedThreadPool、newScheduledThreadPool、newSingleThreadScheduledExecutor
public class Executors {
// 第一种:定长线程池--拥有固定线程数量的线程池,若无任务执行,线程会一直等待
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
// 第二种:容量为1的线程池--线程池中只有一个工作线程
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
// 第三种:可缓存的线程池 -- 被创建的线程若处于空闲状态,60s后自动销毁
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
// 第四种:可调度的线程池 -- 需要指定核心工作线程数量,用于执行调度任务
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
// ScheduledThreadPoolExecutor默认的参数如下:
// super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
// new DelayedWorkQueue());
}
// 第五种:容量为1的可调度线程池 -- 只有1个工作线程执行调度任务
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1));
}
}
- Java8又新增了newWorkStealingPool类型的线程,它是一种具有抢占式操作的线程池,有2个方法:一个方法需要指定并行数目,另一个方法默认系统的CPU核的数量(Runtime.getRuntime().availableProcessors())。该类型的线程池属于并行线程池,与前面五种线程池不同之处在于,它不会保证任务的顺序执行,哪个任务抢占到线程就执行哪个。
public static ExecutorService newWorkStealingPool(int parallelism) {
return new ForkJoinPool
(parallelism,
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
public static ExecutorService newWorkStealingPool() {
return new ForkJoinPool
(Runtime.getRuntime().availableProcessors(),
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
4、线程池的使用步骤
从上面的六种线程池类型看,返回值可以分为两大类:ExecutorService接口和ScheduledExecutorService接口,因此如何使用,要先了解这两个接口:
ExecutorService接口源码:
- 该接口继承了Executor 接口,Executor里的execute()方法执行的是Runnable实例,无返回值,但使用会受到局限,可以使用submit()替代。
public interface ExecutorService extends Executor {
// 关闭线程池操作
void shutdown();
List<Runnable> shutdownNow();
boolean isShutdown();
boolean isTerminated();
// 用于等待子线程结束,再继续往下执行
boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException;
// 单个提交任务操作,参数可以是Callable、Runnable等类型的线程,返回Future对象
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
// 批量提交任务操作, invokeAll会保证集合中的任务全部完成再返回,而invokeAny返回集合中的任务的一个,但不知道返回哪一个任务的Future对象
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks) throws InterruptedException;
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException;
<T> T invokeAny(Collection<? extends Callable<T>> tasks) throws InterruptedException, ExecutionException;
<T> T invokeAny(Collection<? extends Callable<T>> tasks, long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException;
}
ScheduledExecutorService接口源码:
- ScheduledExecutorService接口继承了ExecutorService接口,因此ExecutorService接口里的方法均可使用,此外还额外增加了用于延迟或周期性调度任务的方法。
public interface ScheduledExecutorService extends ExecutorService {
// 创建具有延迟的任务
public ScheduledFuture<?> schedule(Runnable command,long delay, TimeUnit unit);
public <V> ScheduledFuture<V> schedule(Callable<V> callable,long delay, TimeUnit unit);
// 创建并执行具有周期频率或延迟的任务
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit);
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
}
A、线程池的创建
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
ExecutorService workStealingPool = Executors.newWorkStealingPool();
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(10);
ScheduledExecutorService singleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor();
B、向线程池提交任务
- 创建线程时,使用Runable接口实现线程的(继承Thread类本质上属于使用了Runable接口):
// 执行任务方式一
fixedThreadPool.execute(new MyThread());
// 执行任务方式二
Future future = fixedThreadPool.submit(new MyThread());
logger.info("任务是否执行完毕:" + future.get());
- 创建线程时,使用Callable接口实现线程的:
Future future = fixedThreadPool.submit(new MyCallable());
logger.info("获取返回值:" + future.get());
注意:
1.使用具有返回Future对象的submit()方法,在Runable接口,可以通过Future对象的get()方法判断任务是否执行完毕(返回null表示任务已执行完毕~):
2.Callable接口中的call()方法有返回值,也可以通过Future对象的get()方法去获取:
C、线程池的关闭
程序通过 main() 方法启动,当主线程退出了程序后,如果ExecutorService继续存在,程序将继续保持运行状态,而ExecutorService中存在的活动线程是不会被JVM关闭的,因此需要shutdown()或shutdownNow()进行关闭。
- shutdown():不会立即关闭线程池,而是不再接受新提交的任务,等待线程池中没有执行完的任务线程执行任务完毕后关闭线程池;
- shutdownNow():尝试中断所有的线程(不管是正在执行任务的线程还是已经执行完任务的线程),返回正在执行任务的列表,但无法保证正在执行任务的线程能否被终止;
- isShutdown():只要执行了shutdown()或shutdownNow(),返回true;
- isTerminated():当所有的任务都终止时,才返回true;
举例:
有一个短任务和一个长任务,测试关闭线程池(以newFixedThreadPool为例):
@Test
void testExecutors() throws ExecutionException, InterruptedException {
// 短任务
Thread shortTask = new Thread(
() -> logger.info("shortTask正在调用线程:" + Thread.currentThread().getName())
);
// 长任务
Thread longTask = new Thread(
() -> {
logger.info("longTask正在调用线程:" + Thread.currentThread().getName());
try {
TimeUnit.SECONDS.sleep(5); //休眠5秒
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 创建FixedThreadPool类型的线程池
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(10);
for (int i = 0; i < 5; i++) {
fixedThreadPool.submit(shortTask); //提交短任务
}
logger.info("第一次判断是否Shutdown了线程池:" + fixedThreadPool.isShutdown());
logger.info("第一次判断是否终止了所有的任务:" + fixedThreadPool.isTerminated());
// 继续提交长任务(模拟线程池有个正在执行任务的线程还未执行完任务)
fixedThreadPool.submit(longTask);
// Shutdown当前的线程池
fixedThreadPool.shutdown();
logger.info("第二次判断是否Shutdown了线程池:" + fixedThreadPool.isShutdown());
logger.info("第二次判断是否终止了所有的任务:" + fixedThreadPool.isTerminated());
// shutdown线程池后测试一下能否继续提交新的任务,结果报拒绝异常,说明无法继续提交新任务到线程池
// fixedThreadPool.submit(shortTask);
// 延迟5秒,模拟线程池可以执行完长任务longTask
// TimeUnit.SECONDS.sleep(5);
fixedThreadPool.awaitTermination(5,TimeUnit.SECONDS);
logger.info("第三次判断是否Shutdown了线程池:" + fixedThreadPool.isShutdown());
logger.info("第三次判断是否终止了所有的任务:" + fixedThreadPool.isTerminated());
}
结果:
遇到的问题:
在线程池shutdown()之后再往线程池加入新任务,线程池执行拒绝策略:抛异常!!
上述代码将shutdown()改为shutdownNow()再进行测试,结果如下:
- 依旧会拒绝在线程池shutdownNow()后往线程池加入新的任务;
- 休眠5s时,休眠状态被打断,改成休眠3s依旧是,说明了shutdownNow()尝试并成功打断了线程池中所有正在执行任务的线程(包括longTask线程,休眠线程等)
【不积硅步无以至千里,共同进步吧~】