目录
创建线程的4种方式
众所周知,创建线程由以下4种方式:继承Thread类创建线程,实现Runnable接口创建线程,使用CallBack和FutureTask创建线程,通过线程池创建线程。由于创建一个线程实例的时间成本及资源消耗都很高,在实际开发中我们一般使用线程池来创建线程。java提供了一个名为Executors的静态工厂类来创建不同的线程池,其中包含几种常见的线程池。
几种常见的线程池
- FixedThreadPool:核心线程数和最大线程数一样,是一个固定长度的线程池
- CachedThreadPool:可缓存线程池,线程数可以无限增加,当线程闲置时可以对线程进行回 收,该线程池的长度是不固定的。
- ScheduledThreadPool:是一种支持定时或者周期性任务的线程池
- SingleThreadExecutor:该线程池只有一个线程,如果线程在执行任务过程中发生了异常,会重新创建一个新线程继续后续任务的执行
- SingleThreadScheduledExecutor:是ScheduledThreadPool的一种特例,其内部只有一个线程
- ForkJoinPool:适合执行可以产生子任务的任务,有一个Task,它可以产生三个子任务,三个子任务并行执行完之后将结果汇总给Result
使用CountDownLatch实现主线程与子线程的同步
有时候我们需要所有子线程任务执行完之后主线程再继续执行,例如:我们需要解析多个文件的内容,并将解析出来的内容保存到数据库。此时我们就可以使用多线程来实现,使用多个子线程去解析文件,等所有子线程解析完毕之后,主线程再将解析的结果批量保存。
CountDownLatch相当于是一个计数器,有一个int类型的入参,代表了需要等待的线程数量,允许一个或者多个线程去等待其他线程执行完毕,主要有以下几个方法:
void await() | 使当前线程到同步队列中等待,直到latch的值变为0或者当前线程被中断, 此时才会被唤醒 |
boolean await(long timeout, TimeUnit unit) | 有超时时间的等待方法,超过该时间后当前线程也会被唤醒 |
void countDown() | 使latch的值减1,当latch值变为0时,会唤醒所有等待该latch的线程 |
long getCount() | 获取latch的值 |
主线程捕获子线程的异常
由于线程的run()方法没有throws语句,所以并不会抛出checked异常,至于RuntimeException这样的unchecked异常,由于线程是由JVM进行调度的,主线程是不会捕获到子线程的运行时异常,所以在主线程中使用try-catch语句是无法实现捕获子线程异常的。
此时如果主线程需要捕获子线程的方法,可以调用ThreadPoolExecutor.submit()方法,此方法可以获得一个线程执行结果的Future对象,使用Future.get()方法时可以捕获ExecutionException,从而知道子线程中发生了异常。
代码实现
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
5, //核心线程数,即使空闲也不会被回收
10, //最大线程数
1, //线程最大空闲时长
TimeUnit.MINUTES, //线程最大空闲时长类型
new LinkedBlockingQueue<>(5),//阻塞队列的长度
new CallerRunsPolicy() //拒绝策略,调用者执行策略
);
CountDownLatch latch = new CountDownLatch(list.size());
List<Attach> vector = new Vector<>();
Attach att = new Attach();
for (File file : list) {
Future future = threadPool.submit(() -> {
att = attachService.identify(file);
latch.countDown();
vector.add(att);
});
//捕捉子线程异常,防止子线程报错导致主线程无法继续进行
try {
future.get(5,TimeUnit.SECONDS);
}catch (ExecutionException e){
log.error("1、捕捉到子线程异常:" , e);
latch.countDown();
} catch (InterruptedException e) {
log.error("2、线程中断异常:" , e);
latch.countDown();
} catch (TimeoutException e) {
log.error("3、子线程执行结果获取超时:" , e);
latch.countDown();
}
}
try {
latch.await();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
threadPool.shutdown();
//批量保存入库操作
线程池的标准创建方式
大部分的企业都禁止使用Executors创建线程池,例如阿里巴巴开发手册就明文规定了禁止使用Executors创建线程池。要求通过标准构造器ThreadPoolExecutor去创建线程池,其中有一个较为重要的构造方法:
public ThreadPoolExecutor(int corePoolSize,//核心线程数,即使线程空闲也不会回收
int maximumPoolSize,//最大线程数
long keepAliveTime,//线程最大空闲时间
TimeUnit unit,//空闲时间类型(毫秒、秒、分钟...)
BlockingQueue<Runnable> workQueue,//阻塞队列
ThreadFactory threadFactory,//新线程的产生方式
RejectedExecutionHandler handler //拒绝策略
)
注意事项:
当前工作线程多于核心线程数,且小于最大线程数时,新的任务将会暂时保存到阻塞队列中,只有当阻塞队列满了之后才会创建新的线程去执行任务,所以如果核心线程数、阻塞队列、最大线程数等参数配置不当会出现任务不能被正常执行的问题。
/**
* 做一个极限测试:
* 此时只有一个核心线程,往线程池提交了5个任务,而阻塞队列长度为100
* 这时候由于阻塞队列没有满,所以不会创建一个新的线程去执行剩下的4个任务
* 剩下的4个任务只有等第一个任务执行完才可以执行
*/
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
1, //核心线程数,即使空闲也不会被回收
100, //最大线程数
100, //线程最大空闲时长
TimeUnit.MINUTES,
new LinkedBlockingQueue<>(100)//阻塞队列的长度
);
for (int i = 0; i < 10; i++){
threadPool.execute(()->{
System.out.println(Thread.currentThread().getName());
try {
//工作线程睡眠无限长的时间
Thread.sleep(Long.MAX_VALUE);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
});
}
线程池的拒绝策略
在线程池的阻塞队列(LinkedBlockingQueue)为有界队列时,如果队列满了,提交任务到线程池的时候就会被拒绝。总体来说,任务被拒绝有两种情况:
- 线程池已经被关闭了
- maximumPoolSize(最大线程数)已满且LinkedBlockingQueue(阻塞队列)已满
当任务被拒绝时,线程池都会调用RejectedExecutionHandler实例的RejectedExecutionHandler方法。RejectedExecutionHandler是拒绝策略的接口,JUC提供了以下几种实现:
- AbortPolicy:拒绝策略
线程池的默认拒绝策略,如果队列满了,新任务会被拒绝,且抛出异常:RejectedExecutionException,该异常是运行时异常,很容易忘记捕获。如果关心任务被拒绝的事件,需要在提交任务时捕获该异常。
- DiscardPolicy:抛弃策略
是AbortPolicy的安静版本,如果队列满了,新任务会被直接丢弃,且不会抛出异常
- DispartOldestPolicy:抛弃最老任务策略
如果队列满了,将会抛弃最早进入队列的任务,从队列中腾出空间,再尝试加入队列。因为队列时先进先出,队头的任务是最老的,所以每次都是移除队头的任务后再尝试入队。
- CallRunsPolicy:调用者执行策略
在新任务被添加到线程池是,如果添加失败,那么提交任务的线程就会自己去执行该任务,不会使用线程池中的线程去执行新的任务。
该策略有两个好处:
- 新任务不会被丢弃,不会造成业务的损失
- 负责提交任务的线程去执行任务,那么此时负责提交任务的线程就得负责执行任务,而执行任务又是比较耗时的,这时候便不会再有新任务提交(该线程去执行任务了),相当于是一个缓冲,减缓了提交任务的速度,可以利用这段时间执行掉一些任务,腾出线程池的一部分空间,用于接收新的任务。
- 自定义策略
如果以上拒绝策略都不符合业务需求,那么也可以自定义一个拒绝策略,实现RejectedExecutionHandler接口,重写rejected方法。