线程池简单介绍以及使用线程池的几种问题及解决方案
线程池任务执行源码
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
//当前线程数小于核心线程数——新建核心线程
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
//当前线程数大于或等于核心线程数,且线程池处于Running状态——任务加入队列
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//队列添加失败(队列满了)——新增非核心线程
else if (!addWorker(command, false))
reject(command);
}
public boolean offerFirst(E e, long timeout, TimeUnit unit)
throws InterruptedException {
if (e == null) throw new NullPointerException();
Node<E> node = new Node<E>(e);
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (!linkFirst(node)) {
if (nanos <= 0)
return false;
nanos = notFull.awaitNanos(nanos);
}
return true;
} finally {
lock.unlock();
}
}
- 可以看到线程池中对线程对象的访问使用了 (final ReentrantLock lock = this.lock;)可重入锁,避免多个线程同时访问统一对象。
线程池的几个参数:
1、corePoolSize
2、runnableTaskQueue
任务队列的几种类型:
(1)ArrayBlockingQueue:数组结构的有界阻塞队列
(2)LinkedBlockingQueue:链表结构的阻塞队列)
(3)SynchronousQueue(不是一种真正的队列,是线程之间的一种移交机制)
(4)priorityBlockingQueue(优先级队列,默认自然排序,支持自定义排序)
3、maximumpoolSize
4、ThreadFactory
5、RejectedExecutionHandler
饱和策略的几种类型
(1)AbortPolicy:默认的,直接抛出异常
(2) CallerRansPllicy:用调用者来执行当前任务
(3)DiscardOldestPolicy:抛弃最老的任务
(4)DiscardPolicy:不处理新来的,丢弃掉
6、keepAliveTime
7、TimeUnit
使用线程池的几种问题
- 线程饥饿死锁
解释:假设极端情况下,单线程的线程池中,一个任务将另一个任务提交到线程池中
,由于是只有一个线程的线程池,所以任务会被加入到任务队列中等待有可用线程,
而由于任务1无法完成就不会归还线程,因为任务1在等待任务2完成。
…同样的
在更大的线程池中,由于正在执行任务的线程都由于等待其他仍处于工作队列中的任务而阻塞
那么会发生同样的问题。这种现象被称为线程饥饿死锁。
解决方案:
只有任务互相独立时,为线程池设置界限才是合理的,如果任务之间存在依赖性,一种解决方案是:使用
无界的线程池搭配SynchronousQueue使用。
另一种配置方法时:使用有界队列,并使用SynchronousQueue作为工作队列
以及调用者运行(Call-Runs)作为饱和策略。
-
运行时间较长的任务
如果线程池中线程数量远小于在稳定状态下执行时间较长的任务的数量,那么最后可能
所有的线程都会运行这些执行时间较短的任务,从而影响整体的响应性
解决方案:
限定任务等待资源的时间,比如:CountDownLatch.await()、Thread.join() -
线程池中的参数“并不是配置了就有效的”
当配置了无界队列时,根据线程池中线程的创建和销毁逻辑,
(新任务到来时候,工作任务达到核心线程数,
并且未达到最大线程数、并且任务队列已经满了,才创建新的线程;
超过核心线程数的线程,在规定的存活时间内没有被使用,则会销毁)
如果使用了无界队列会导致最大线程数、线程存活时间、饱和策略的参数配置失效 -
饱和策略的选择
-
Abort(终止)策略,默认的饱和策略,抛出RejectedException,可以捕获这个异常。
-
Discard(抛弃)策略会悄悄抛弃新的想要加入队列中的任务
Discard-Oldest(抛弃最旧)策略会抛弃下一个将要出队执行的任务
------最好不要和优先级队列一起使用 -
Caller-Runs(调用者运行)策略,实现了一种调节机制,不会抛弃任务,也不会抛出异常。
假设线程池中所有线程都被占用,工作列表也已经满了,下一个新来的任务将会在主线程中执行,
所有主线程执行这个任务的时间段不会有新的任务加入到工作队列中,
新到达的请求将会保存在TCP层的队列中,如果TCP层队列也满了,同样也会开始抛弃请求。
总而言之:这种拒绝策略会将过载的请求逐渐向外蔓延开来—从线程池的工作队列–>应用程序–>
TCP连接层–>客户端。