1.实现线程的三种方式
- 继承Thread类
- 实现Runnable接口
- 实现Callable接口通过FutureTask包装器来创建Thread线程
// 1.继承Thread
threadExtend threadExtend = new threadExtend();
threadExtend.start();
class threadExtend extends Thread {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "线程启动: 继承Thread类");
}
}
// 2.实现Runnable接口
new Thread(() -> System.out.println(Thread.currentThread().getName() + "线程启动: 实现Runnalbe接口")).start();
// 3.实现Callable接口通过FutureTask包装器来创建Thread线程
Callable<Integer> oneCallable = new SomeCallable<>(10);
//由Callable<Integer>创建一个FutureTask<Integer>对象:
FutureTask<Integer> oneTask = new FutureTask<>(oneCallable);
// FutureTask<Integer>是一个包装器,它通过接受Callable<Integer>来创建,它同时实现了Future和Runnable接口。
// 由FutureTask<Integer>创建一个Thread对象:
Thread oneThread = new Thread(oneTask);
oneThread.start();
class SomeCallable<Integer> implements Callable<Integer> {
private Integer result;
public SomeCallable() {
}
public SomeCallable(Integer result) {
this.result = result;
}
@Override
public Integer call() {
System.out.println(Thread.currentThread().getName() + "线程启动: 实现Callable接口");
return result;
}
}
// 或者直接使用Lambda表达式:
int result = 10;
new Thread(new FutureTask<>(()->{
System.out.println(Thread.currentThread().getName() + "线程启动: 实现Callable接口 Lambda表达式");
return result;
})).start();
2.线程池的使用
java通过Executors提供了四个静态方法创建线程池,分别是:
/**
* 实现Runnable的方法
*/
class ThreadRunnalbe implements Runnable {
private static final Logger LOG = LoggerFactory.getLogger(ThreadRunnalbe.class);
private String threadMethod; // 线程池的实现
private Integer index; // 索引
ThreadRunnalbe(String threadMethod, Integer index) {
this.threadMethod = threadMethod;
this.index = index;
}
@Override
public void run() {
LOG.info("{}:线程 = {},index = {}", threadMethod, Thread.currentThread().getName(), index);
}
}
- newCachedThreadPool:创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
ExecutorService es = Executors.newCachedThreadPool();
for (int i = 0; i < 20; i++) {
es.execute(new ThreadRunnalbe("cacheThreadPool", i));
}
es.shutdown();
- newFixedThreadPool :创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
(我的项目中常用的是newFixedThreadPool,可以自定义线程池大小,后期可通过CompletionService来进行管理线程的执行情况)
ExecutorService es = Executors.newFixedThreadPool(10);
for (int i = 0; i < 20; i++) {
es.execute(new ThreadRunnalbe("fixedThreadPool", i));
}
es.shutdown();
- newScheduledThreadPool: 创建一个定长线程池,支持定时及周期性任务执行。
// 定时任务线程池
ScheduledExecutorService es = Executors.newScheduledThreadPool(2);
for (int i = 0; i < 2; i++) {
// 参数1:目标对象
// 参数2:隔多长时间开始执行线程
// 参数3:执行周期
// 参数4:时间单位
es.scheduleAtFixedRate(new ThreadRunnalbe("scheduledThreadPool", i), 5, 10, TimeUnit.SECONDS);
}
- newSingleThreadExecutor: 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
ExecutorService es = Executors.newSingleThreadExecutor();
for (int i = 0; i < 10; i++) {
es.execute(new ThreadRunnalbe("singleThreadExecutor", i));
}
es.shutdown();
3.线程池的管理
- 原生:线程实现Callable接口,Callable接口是有返回值的,ExecutorService.submit方法将返回值封装为Future ,通过循环Future中线程的状态,获取线程的执行情况。(顺序获取,获取方式是阻塞式的,获取不到就阻塞)
ExecutorService executorService = Executors.newCachedThreadPool();
List<Future<Integer>> resultList = new ArrayList<>();
// 创建10个任务并执行
for (int i = 0; i < 10; i++) {
// 使用ExecutorService执行Callable类型的任务,并将结果保存在future变量中
Future<Integer> future = executorService.submit(new SomeCallable<>(i));
// ↑ submit和execute方法的区别见:com.cn.lg.springthreadpool.b.threadpool.ThreadPool.shutdownThreadPool()方法
// 将任务执行结果存储到List中
resultList.add(future);
}
// 遍历任务的结果
for (Future<Integer> fs : resultList) {
try {
LOG.info("执行结果:{}", fs.get()); // 打印各个线程(任务)执行的结果
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
} finally {
// 启动一次顺序关闭,执行以前提交的任务,但不接受新任务。如果已经关闭,则调用没有其他作用。
executorService.shutdown();
}
}
/**
* 实现Callable接口可以获取到返回值
* @param <Integer>
*/
class SomeCallable<Integer> implements Callable<Integer> {
private Integer result;
// 通过构造函数将参数注入
SomeCallable(Integer result) {
this.result = result;
}
/**
* 实现call方法
*/
@Override
public Integer call() {
System.out.println(Thread.currentThread().getName() + "线程启动: 实现Callable接口");
return result;
}
}
- 升级版:通过CompletionService管理
CompletionService将Executor(线程池)和BlockingQueue(堵塞队列)结合在一起,统一使用Callable作为任务的基本单元,整个过程就是生产者不断把Callable任务放入堵塞队列,Executor作为消费者不断把任务取出来运行,并返回结果;
优势:
- 堵塞队列防止了内存中排队等待的任务过多,造成内存溢出(毕竟一般生产者速度比較快,比方爬虫准备好网址和规则,就去运行了,运行起来(消费者)还是比较慢的)
- CompletionService能够实现,哪个任务先运行完毕就返回,而不是按顺序返回,这样能够极大的提升效率;
ExecutorService executor = Executors.newFixedThreadPool(5);
CompletionService<Integer> ecs = new ExecutorCompletionService<>(executor);
// 假设一共有20个任务,每个任务执行1s,使用线程池
int taskNum = 20;
for(int i = 0; i < taskNum; i++) {
final int index = i + 1;
// Callable的call方法能够返回运行的结果
ecs.submit(() -> {
LOG.info("第{}个任务:执行1s,当前线程:{}", index, Thread.currentThread().getName());
Thread.sleep(2000L);
return index; // 因为在CompletionService定义了Integer的返回类型,所以这里需要进行返回
});
}
List<Integer> resultList = new ArrayList<>();
for(int i = 0; i < taskNum; i++) {
try {
Integer result = ecs.take().get(); // 使用阻塞的方式,打印各个线程(任务)执行的结果
resultList.add(result);
} catch (Exception e) {
// 如果执行抛出异常,会在这里捕获
e.printStackTrace();
}
}
LOG.info("获取到的返回结果:{}", resultList.toString());
// 当所有线程执行完毕后,关闭线程池
executor.shutdown();
4.项目中的使用
- 视频切片
需求:用户上传多个视频,需要将所有视频按帧切割成图片。
前提:用户的视频被上传到了OSS存储,对视频切片需要将视频从OSS下载下来,切割完成后将图片上传到OSS并删除本地视频。
实现:使用线程池的方式,每个视频使用单独的线程进行处理,而不是单线程排队处理。
每个线程的实现为:从OSS下载视频到uuid目录,保证多线程下载互不影响,切割出来的图片直接通过流的方式上传到OSS,不占用本地存储,切割完成后删除本地视频及新建的uuid文件夹。(需控制好视频下载和删除的位置,防止不同任务的视频下载到同一目录下,其他线程完成后误删除别的任务的视频,导致别的任务失败) - 从队列从读取任务
需求:队列为点对点通信。队列中存在大量的任务ID,获取每个任务ID,执行对应的业务逻辑,将数据入库。
前提:每个任务耗时较长,队列增长速度较快。
实现:监听器使用线程池,起10个线程实时监听队列,每个监听器中再创建线程池(大小为10),监听器获取到数据后,扔进业务的线程池,异步执行任务,然后继续监听,可以防止监听器阻塞,加快队列的消耗速度。如果业务代码抛出Runnable异常,将异常的任务ID返回给队列重新执行。(通过此方法消耗队列的速度 > 队列的增长速度,毕竟之前是单线程,现在是多线程,还加了分布式,之后的瓶颈就到了数据库那边)
升级:使用微服务,起2个监听器的微服务,两个服务去消耗队列。(需对任务ID使用分布式锁,防止重复执行任务) - ——待补充