目录
前言
并发处理的广泛应用是使得 Amdahl定律代替摩尔定律成为计算机性能发展源动力的根本原因,也是人类压榨计算机运算能力最有力的武器。
1、理解线程池
在Java中,如果每个请求到达就创建一个新线程,创建和销毁线程花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。如果在一个JVM里创建太多的线程,可能会使系统由于过度消耗内存或“切换过度”而导致系统资源不足,为了解决这个问题,就有了线程池的概念,线程池的核心逻辑是提前创建好若干个线程放在一个容器例如HashSet中,如果有任务需要处理,则将任务直接分配给线程池中的线程来执行就行,任务处理完以后这个线程不会被销毁,而是等待或者继续执行后续任务,这样就避免了上述大量线程创建和销毁的开销。
1.1、线程创建过多的代价
线程虽然能帮我们更好的利用cpu资源,但是线程创建过多也是有性能消耗的。因为java的线程实现模型是用户线程和内核线程1:1的实现方式,
挂起到唤醒的过程都是系统内核级别的切换,做的工作开销如下:
-
1、太多的线程本身也需要消耗内存空间;
-
2、用户态到内核态的频繁切换耗时;
-
3、cpu是基于时间片的运行,中断调度后需要知道数据从哪里加载?指令从哪里运行?所以需要保存cpu寄存器和程序计数器指令等数据,保证原来的任务的执行不变,方便线程被唤醒后再次从原来的地方执行;
-
4、切换将会使cpu加载的缓存失效,影响了程序读取数据的效率,缓存行的填充优化丧失了;
1.2、为什么要使用线程池?
-
避免有限的资源的过分使用:控制资源总数,解决硬件资源的瓶颈;
-
提高线程的控制管理:防止线程的不可控,过度切换,控制最大的并发数,按需来设置参数,功能更灵活;
-
提高响应速度:任务无需等待线程的创建直接执行;创建时间t1,执行时间t2,销毁时间t3;节省t1和t3;
-
限流:流量过大,可以使用拒绝策略;
2、JDK中的线程池
为了使用的方便,JDK为我们提供了几种不同线程池的实现:
类型一:
newFixedThreadPool
:固定数量线程池
-
简介:该方法返回一个固定数量的线程池,线程数不变,当有一个任务提交 时,若线程池中有空闲线程,则立即执行;若没有,则会被暂存在任务队列中,等待有空闲线程去执行;
-
适用场景:机器负载比较大的场景,为了更佳合理的使用资源,需要控制机器线程的数量;
-
执行长期的任务;
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()); //无界队列,可能堆积请求,发生OOM
}
类型二:newSingleThreadExcutor:单个线程线程池
-
简介:创建一个线程的线程池,一个一个任务的执行;若没有空闲线程任务则暂存任务队列中。
-
使用场景:让所有的任务按照fifo提交的顺序来执行;
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>())); //无界队列,可能堆积请求,发生OOM
}
类型三:newCacheThreaPool: 缓存线程池-没有核心线程-可以弹性伸缩
-
简介:返回一个可根据实际情况调整线程个数的线程池,不限制最大线程数量,若用空闲的线程则执行任务,若无任务则不创建线程。并且每一个空闲线程JVM会在60秒后自动回收。
-
使用场景:短小的任务,负载较轻的机器;
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE, //线程的创建个数不限,可能耗尽内存oom,过度切换,影响性能
60L, TimeUnit.SECONDS, //60s 回收一次,keepalive 监控线程超时时间空闲就被回收;
new SynchronousQueue<Runnable>()); //不存储任务
}
类型四:newWorkStealingPool fork/join框架
-
简介:一个可以进行任务拆分合并的计算框架线程池
-
使用场景:分布式计算,计算任务量较大;
public static ExecutorService newWorkStealingPool() {
return new ForkJoinPool
(Runtime.getRuntime().availableProcessors(),
ForkJoinPool.defaultForkJoinWorkerThreadFactory,
null, true);
}
类型五:ScheduleThreadPoolExecutor
-
简介:一个可以指定线程的数量的线程池,但是这个线程池还带有 延迟和周期性执行任务的功能,类似定时器。
-
使用场景:周期性的执行任务;
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize,
Integer.MAX_VALUE, //线程的创建个数不限,可能耗尽内存oom,过度切换,影响性能
0, NANOSECONDS,
new DelayedWorkQueue());
}
3、线程池的实现原理
Executers 是工具类,主要用来创建线程池对象,ThreadPoolExecutor 是线程池的核心,提供了线程池的实现。
3.1、线程池的内部组件
-
corePool:核心线程池的大小;
-
maximumPool:最大线程池的大小;
-
BlockingQueue:用来暂时保存任务的工作队列;
-
rejectedExecutionHandler:拒绝策略;
new ThreadPoolExecutor(
int corePoolSize, //核心线程数,最小线程
int maximumPoolSize, //最大的线程数
long keepAliveTime, //线程存活时间
// (1)当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程
// (2)当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭
TimeUnit unit, //存活时间单位
BlockingQueue workQueue, //线程排队队列
ThreadFactory threadFactory, //线程创建工厂,设置线程的名字,好定位问题
RejectedExecutionHandler handler //饱和拒绝策略
);
线程池线程的初始化的时机
线程池创建以后是没有线程的,只有在提交任务后才会创建线程,可以使用
线程池的预热:
-
prestartCoreThread()初始化一个线程;
-
prestartAllCoreThreads();初始化所有的线程;
-
allowCoreThreadTimeOut (true); 表示核心线程数也可以被回收;
tips:线程池刚初始化的时候是没有创建线程的,只有在传入任务进行预热后才会创建core线程,当队列满的时候,才会创建maxCore线程;
那么当我们把一个任务提交给线程池以后,线程池的处理是怎样的呢?
3.2、线程池执行流程
1.当线程池小于corePoolSize时,新提交任务将创建一个新线程执行任务,即使此时线程池中存在空闲线程;
2.当线程池达到corePoolSize时,新提交任务将被放入workQueue中,等待线程池中任务调度执行;
3.当workQueue已满,且maximumPoolSize>corePoolSize时,未达到最大的线程数,新提交任务会创建新线程执行任务;
4.当提交任务数超过maximumPoolSize时,新提交任务由RejectedExecutionHandler处理;
5.当线程池中超过corePoolSize线程,非核心线程空闲时间达到keepAliveTime时,关闭空闲线程;
6.当设置allowCoreThreadTimeOut(true)时,线程池中核心线程空闲时间达到keepAliveTime也将关闭。
4、 线程池的阻塞队列
(1) 直接提交-SynchronousQueue
直接提交-SynchronousQueue,直接提交策略时线程池不会对任务进行缓存,对于新提交的任务,如果线程池中没有空闲的线程,就创建一个新的线程去处理,线程池具有无限增长的可能性。对于
“脉冲式”流量请求的情况可能是致命的,对导致系统oom,或者线程数过多过度切换导致系统瘫痪;
(2) 有界队列-ArrayBlockingQueue
新提交的任务,当线程池中线程达到corePoolSize时,新进任务被放在队列里排队等待处理。
使用大型队列+小型池:可以最大限度地降低 CPU 使用率、降低操作系统资源和上下文切换开销,于此同时也降低吞吐量。如果任务频繁的I/O繁阻塞,增加任务的耗时。
使用小型队列+大型池:CPU使用率较高;池子需要适量,否则容易出现oom或者线程的切换导致的系统崩溃。
(3) 无界队列- LinkedBlockingQueue
使用无界队列将导致在所有 corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize,此时maximumPoolSize 的值也就无效了。队列大小默认
为:
Integer.MAX_VALUE
;
(4) PriorityBlockingQueue
其所含对象的排序不是FIFO,而是依据对象的自然顺序或者构造函数的Comparator决定。
线程数量随着阻塞队列中任务的增加的变化趋势
4.1、ArrayBlockingQueue源码解析
4.1.1、数据结构和实现原理
-
Object item[]; 存放数据的数据结构-数组;
-
ReentrantLock:保证出入队列线程安全;
-
Condition:等待队列实现阻塞:
-
Condition notEmpty.await():队列满条件等待;
-
Condition notFull.single():队列未满条件入队;
-
-
putIndex; 数据如队列游标;
-
takeIndex; 数据出队列游标;
4.1.2、方法使用
|
抛出异常
|
特殊值
|
阻塞
|
超时
|
入队
|
add(e)
成功:true
队列满抛出异常
实际掉的offer(e)
|
offer(e)
入队成功返回:true
入队失败返回:false
|
put(e)
队列满阻塞生产线程
|
offer(e, time,unit)
阻塞超时时间后直接退出
|
出队
|
remove(e)
队列空:抛异常不存在
成功:true
实际掉的poll()
|
poll(timeout)
队列不为空:取出e
空:返回null
|
take()
消费者阻塞直到有数据
|
poll(time,unit)
等待指定的时间,然后再去获取元素返回
|
使用场景:
-
生产-消费者模型,系统解耦;
-
异步消息的传递通信;
-
脉冲式流量整形;
-
异步处理;
4.1.3、ArrayBlockingQueue源码解析
ArrayBlockingQueue类图如下:实现了BlockingQueue,继承了Queue队列。
ArrayBlockingQueue的类结构:
-
数组Object[] item;
-
Reentrantlock:保证线程安全;
-
Condition: 实现队列里元素满和空时的阻塞和唤醒;
删除和添加数据的索引下标:
-
takeIndex
-
putIndex
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
/** The queued items */
final Object[] items; //存储元素的数组
/** items index for next take, poll, peek or remove */
int takeIndex; //获取元素的游标
/** items index for next put, offer, or add */
int putIndex; //存放元素的游标
/** Number of elements in the queue */
int count; //阻塞队列元素的总个数
/** Main lock guarding all access */
final ReentrantLock lock; //防治并发操作的线程安全
/** Condition for waiting takes */
private final Condition notEmpty; //等待获取元素的线程
/** Condition for waiting puts */
private final Condition notFull; //等待存放元素的线程
向队列中put()添加元素的过程:
-
如果队列满,则调用await()方法阻塞存放数据的线程,数据出对后再唤醒入队数据的线程;
-
如果队列空,则调用await()方法阻塞获取数据的线程,数据入队后再唤醒获取数据的线程;
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await(); //数组中元素已满,调用阻塞;
enqueue(e); //数组元素未满,入队列
} finally {
lock.unlock();
}
}
*/
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0; //如果数据已满,则putIndex=0,从头开始
count++;
notEmpty.signal(); //入队唤醒获取元素的线程
}
出队列take()获取元素:使用同步控制,如果元素队列不用空,则出队列;否则等待阻塞等待入队元素;
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == 0)
notEmpty.await(); //数组中元素未空
return dequeue(); //出队列
} finally {
lock.unlock();
}
}
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
notFull.signal(); //出对唤醒存放元素的线程
return x;
}
remove()移除一个元素:
public boolean remove(Object o) {
if (o == null) return false;
final Object[] items = this.items;
final ReentrantLock lock = this.lock;
lock.lock(); //同步控制
try {
if (count > 0) {
final int putIndex = this.putIndex;
int i = takeIndex;
do {
if (o.equals(items[i])) {
removeAt(i); //移除一个元素
return true;
}
if (++i == items.length)
i = 0;
} while (i != putIndex);
}
return false;
} finally {
lock.unlock();
}
}
5、线程池的拒绝策略
-
策略一:AbortPolicy:丢弃任务并抛出RejectedExecutionException异常【jdk默认策略】;
-
策略二:DiscardPolicy:直接丢弃新来的任务,队列尾的任务,但是不抛出异常;
-
策略三:DiscardOldestPolicy:丢弃队列最前面的任务,执行后面的任务;
-
策略四:CallerRunsPolicy:谁提交谁来执行,不用线程池中的线程执行,会阻塞入口;
-
策略五:如果觉得系统提供的策略不好,实现自己的饱和策略,实现 RejectedExecutionHandler ,日志和存储等操作;
实例测试
使用策略一:AbortPolicy
AbortPolicy:丢弃任务并抛出RejectedExecutionException异常
【jdk默认策略】;
//创建一个核心线程为1,最大线程为2,核心线程存活时间为1s,有界队列为3的等待队列,采用默认的AbortPolicy 拒绝策略;
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 2, 3000,
TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(3),new ThreadPoolExecutor.AbortPolicy());
AbortPolicy策略运行结果:
结果分析:从执行结果来看,任务任然执行完成了任务0-1-2-3-4 ,总过5个,任务5-6-7-8-9被抛弃了。AbordPolicy策略是,线程达到最大核心线程1个pool-1-thread-1时,放入队列,队列满,又创建了pool-1-thread-2,此时新提交的任务将会直接丢弃,且抛出RejectedExecutionException异常。
使用策略二:DiscardPolicy
DiscardPolicy:直接丢弃新来的任务,队列尾的任务,但是不抛出异常;
//创建一个核心线程为1,最大线程为2,核心线程存活时间为1s,有界队列为3的等待队列,采用DiscardPolicy 拒绝策略;
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 2, 3000,
TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(3),new ThreadPoolExecutor.DiscardPolicy());
DiscardPolicy策略执行结果:
结果分析:从结果来看,任务任然执行完成了任务0-1-2-3-4-5 ,总过6个,任务6-7-8-9被抛弃了。但是和策略一不同的地方是没有抛出拒绝异常,且丢弃的是后来最新提交的任务。
使用策略三:DiscardOldestPolicy
DiscardOldestPolicy:丢弃队列最前面的任务,执行后面的任务;
//创建一个核心线程为1,最大线程为2,核心线程存活时间为1s,有界队列为3的等待队列,采用DiscardOldestPolicy拒绝策略;
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 2, 3000,
TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(3),new ThreadPoolExecutor.DiscardOldestPolicy());
DiscardOldestPolicy策略执行结果:
结果分析:线程池执行来0-1-5-7-8-9任务,抛弃来2-3-4-6四个任务,没有抛出异常且丢弃的是队列中老的请求。
策略四:CallerRunsPolicy
CallerRunsPolicy:谁提交谁来执行,不用线程池中的线程执行,会阻塞入口;
//创建一个核心线程为1,最大线程为2,核心线程存活时间为1s,有界队列为3的等待队列,采用CallerRunsPolicy拒绝策略;
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 2, 3000,
TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(3),new ThreadPoolExecutor.CallerRunsPolicy());
CallerRunsPolicy策略执行结果:
结果分析:所有的10个任务全部执行,当队列满时,不想放弃执行任务。但是由于池中已经没有任何资源了,那么就直接使用调用该execute的线程本身main来执行。同时也减缓来请求的提交速度,达到来反控的目的。
策略5:自定义策略
-
一般不推荐直接丢弃数据,可以使用阻塞生产者提交任务的方法来暂缓任务的提交;
-
如果对数据的要求比较严格:选择有界队列,重写拒绝策略,报警机制 + 日志记录 + 数据库记录;
//todo 方法二 可以阻塞生产者的线程池
ExecutorService executorService = new ThreadPoolExecutor(20, 50, 2, TimeUnit.HOURS,
new ArrayBlockingQueue<Runnable>(100), new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
if (!executor.isShutdown()) {
try {
/**
* 阻塞生产者
*/
executor.getQueue().put(r); //todo 阻塞队列阻塞生产者
System.out.println("生产者阻塞");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
6、线程池异常处理
-
1. 不带返回值在run方法里捕获所有的异常;
-
2. 带返回值的通过future.get()获取异常,如果不调用该函数异常就不会打印出来,消失在jvm了
-
3. 重写线程池里的afterExecute()方法捕获异常;
-
4. 重写自己的uncaughtException();
方案一:没有返回值的情况,在我们提供的run方法中捕获所有的异常,包括未检测异常;
/**
* 异步消息处理
*
* @param message
*/
public void processAsync(final String message) {
wrapper.submit(new Runnable() {
@Override
public void run() {
try { //捕获所有的异常
process(message);
} catch (Exception e) {
logger.error("B2C支付消息队列消费失败: message={}", message, e);
}
}
});
}
方法二:对于有返回值的我们可以通过future.get(),来获取其异常信息;
这里需要对单个的请求进行try-catch,否则会把所有的请求当作异常来处理,
//todo 异步且得到返回值的情况
public void getUserInfoLevel2() {
Callable<Integer> userInfo = new Callable<Integer>() {
//@Override
public Integer call() throws Exception {
return (Integer) userAccountService.getAccountInfo();
}
};
FutureTask<Integer> userInfoTask = new FutureTask<Integer>(userInfo);
executorService.submit(userInfoTask);
//阻塞的等待结果,所以需要有超时机制
//超时后就返回null,但是服务端是完成了的;
try {
result.put("order", userInfoTask.get(TIME_OUT, TimeUnit.MILLISECONDS));
} catch (Exception e) {
e.printStackTrace();
}
方案三:重写ThreadPoolExcutor中的afterExcutor()方法;
class ExtendedExecutor extends ThreadPoolExecutor {
// ...
protected void afterExecute(Runnable r, Throwable t) { //beforeExecute和afterExecute都没有实现,我们可以搞事情
super.afterExecute(r, t);
if (t == null && r instanceof Future<?>) {
try {
Object result = ((Future<?>) r).get();
} catch (CancellationException ce) {
t = ce;
} catch (ExecutionException ee) {
t = ee.getCause();
} catch (InterruptedException ie) {
Thread.currentThread().interrupt(); // ignore/reset
}
}
if (t != null)
System.out.println(t); //打印异常信息
}
}
方案四:设置自己的异常捕获class:UncaughtException()
4.1 @Async注解没有返回值的情况异常处理:
@Service
public class MyExceptionHandler implements AsyncUncaughtExceptionHandler {
@Override
public void handleUncaughtException(Throwable ex, Method method, Object... params) {
System.out.println("异常信息:Exception message - " + ex.getMessage());
System.out.println("方法名:Method name - " + method.getName());
for (Object param : params) {
System.out.println("参数:Parameter value - " + param);
}
}
}
4.2 普通的线程的异常:
public class RewriteUncatchtExceptionHandler implements Thread.UncaughtExceptionHandler {
public void uncaughtException(Thread t, Throwable e) {
System.out.println("我捕获到了线程池的异常");
}
}
public class Task implements Runnable {
public void run() {
System.out.println("执行任务");
int num = Integer.parseInt("TT");
}
}
public void catchSingleThread() {
Task task = new Task();
Thread thread = new Thread(task);//线程设置异常处理
thread.setUncaughtExceptionHandler(new RewriteUncatchtExceptionHandler());
thread.start();
}
7、线程池的使用技巧?
核心线程数的确定:
-
如果是cpu密集型:线程数 Ncpu + 1; 为什么+1;防止 页缺失[数据在磁盘而不在内存],线程被挂起;过多引起cpu过载及调度切换
-
如果是io密集型:适当增大线程数,一般设置为核心的2倍,充分利用cpu,如依赖数据库,读文件等;
队列的选择:
-
如果任务量小:选择无界队列;
-
任务量大:选择有界队列;
拒绝策略:
-
一般选举阻塞生产者提交任务的速度来控制暂缓任务的提交;
-
如果对数据的要求比较严格:选择有界队列,重写拒绝策略,报警机制 + 日志记录 + 数据库记录;
线程的提交:
需要返回值:submit()
-
Public Future<> submit(Callable task);
不需要返回值:execute():
-
Public void execute(new Runnable());
使用场景:
一般可以异步处理的task 【
比较耗时
+
不希望阻断主流程
】都会用线程池来完成。
-
发通知消息类(短信/邮件等功能);
-
记录日志;
-
发优惠券;
-
定时任务;
用线程池建议:
(1)、获取单例对象需要保证线程安全,其中的方法也要保证线程安全。
-
说明:资源驱动类,工具类,单例工厂都需要注意
(2)、创建线程或线程池时请指定有意义的线程名称 ,方便出错时回溯
public void setThreadName() {
//todo 利用ThreadFactory设置线程的名字
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("message-handler-thread-%d").build();
ExecutorService executorPool = new ThreadPoolExecutor(50, 100, 1, TimeUnit.HOURS,
new ArrayBlockingQueue<Runnable>(1000), namedThreadFactory);
executorPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + ": 线程名字测试");
}
});
}
(3)、线程资源必须通过线程池提供,不允许在应用中自行显示的创建线程。
避免:
-
创建线程的不可控,无法管理;
-
频繁创建和销毁线程的消耗,上下文的切换;
-
资源的有限;
(4)、线程池不允许使用Executor去创建,使用ThreadPoolExecutor的方式,这样的处理让程序员理解线程池的运行规则,规避资源耗尽的风险。
ExecutorService threadpool = new ThreadPoolExecutor(
int corePoolSize, //核心线程数,最小线程
int maximumPoolSize, //最大的线程数
long keepAliveTime, //线程存活时间
// (1)当线程池中超过corePoolSize线程,空闲时间达到keepAliveTime时,关闭空闲线程
// (2)当设置allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭
TimeUnit unit, //存活时间单位
BlockingQueue workQueue, //线程排队队列
ThreadFactory threadFactory, //线程创建工厂,设置线程的名字,好定位问题
RejectedExecutionHandler handler //饱和拒绝策略
);
Executor的弊端:
FixedThreadPool和SingleThreadPool:
-
允许的请求队列的长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM;
CacheThreadPool和ScheduleThreadPool:
-
允许线程的创建数量为Integer.MAX_VALUE,可能会创建大量的线程,导致OOM;
8、问题思考
思
考:
为什么要自定义线程池?
个人认为主要是为了
一切可控,使用一个不明确或者不可控的参数构建一个线程池是有风险的。我们在写代码的时候对要处理的任务是有一定的了解的,比如并发量多大?数据量多大?根据这些信息就大概能知道任务队列定义多长合适,而不是采用默认的无界阻塞队列,一定要做到心中有数,这样出现问题的时候才有章可循,有法可解。
同时,对任务的特征也有所了解,
比如是否要调用远程HTTP服务?是否写磁盘有IO阻塞?还是只是转换数据、处理数据,另外所部署的服务器的硬件性能咋样?是否依赖数据库?这些都能作为定义线程个数的一些参考。
自己设置线程池的好处?
1、无界队列变为有界队列,防治OOM;
2、重写拒绝策略,出现了丢弃的任务需要做补偿,打印日志-落库持久化等;
3、控制线程的个数;保障cpu系统的稳定运行;
4、控制更加自由灵活:在任务执行出错了,可能更灵活地控制处理错误,比如记录错误日志、执行任务前以及执行任务后的清理操作;
9、小结
使用多线程来实现异步方式处理多任务已经成为互联网开发的一种常用的手段。一般在面对大并发的请求,首先想到的是用多线程来实现异步处理,例如将io线程和业务线程分开;实时业务和延时任务隔离等等,从而使得即便是一次请求阻塞了也不会影响到其他的请求的执行,提高效率,同时也避免了线程的频繁创建与销毁一般都使用线程池。
水滴石穿,积少成多。学习笔记,内容简单,用于复习,梳理巩固。