在并发条件下,一个请求一个线程会有很多的问题。
- 线程的创建是占用内存空间的,过得的线程有可能导致内存溢出
- 线程处理的速度不一样,有些处理的快,有些处理的慢,处理快的空闲的线程等待被销毁,非常浪费线程的性能。
- 线程的创建和销毁都要花很多时间,如果使用线程的时间比原本花费的时间要长,那么很明显是不划算的
所以使用线程池来管理所有的线程是很有必要的。
可以完成以下的几个目标
- 管理线程的数量
- 省去线程创建的时间
- 时刻管理线程,尽量使线程忙碌起来,提高 CPU 的运行效率。
在使用工厂方法,创建出来的线程池,底层都是 ThreadPoolExecutor,实现方法如下
- newFixedThreadPool 固定线程数的线程池,将会返回一个 corePoolSize 和 maximumPoolSize 相等的线程池
- newCachedThreadPool 非常有弹性的线程池,对于新的任务,此时线程池里没有空闲线程,线程池会毫不犹豫的创建一条新的线程处理这个任务
- SingleThreadExecutor 使用单个 worker 线程的 Executor
这个类是属于前面所说的线程池的具体实现
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}- corePoolSize 核心线程数量
- maximumPoolSize 最大线程数量
- keepAliveTime 允许线程空闲时间
- unit 时间对象
- workQueue 阻塞队列
- threadFactory 线程工厂
- handler 任务拒绝策略
在这几个参数中,有几个值得注意的地方
- 如果运行线程的数量少于核心线程数量,则创建新的线程处理请求
- 如果运行线程的数量大于核心线程数量,小于最大线程数量,则当队列满的时候才创建新的线程
- 如果核心线程数量等于最大线程数量,则创建固定大小的连接池
- 如果设置了最大线程数量为无穷,那么允许线程池适合任意的并发数量
- 当线程数大于核心线程数,如果空闲时间已超过了,那么该线程将会销毁
- 同步移交:不会放到队列中,而是等待线程执行它。如果当前线程没有执行,很可能会新开一个线程执行。
- 无界限策略:如果核心线程都在工作,该线程将会放到队列中。所以线程数不会超过核心线程数。
- 有界限策略:可以避免资源耗尽,但是在一定程度上减低了吞吐量
当线程关闭或者线程数量满了和队列饱和了,都有拒绝任务的情况
AbortPolicy直接抛出异常CallerRunsPolicy使用调用者的线程来处理DiscardPolicy直接丢掉这个任务DiscardOldestPolicy丢掉最老的任务
- shutdown():线程池的状态立刻变为 Shutdown,而调用 shutdownNow(),线程池状态立刻变为 STOP
- shutdown():等待任务执行完才中断线程,而 shutdownNow() **不等任务执行完就中断了线程
线程池就有点儿像一个生产者消费者模式
- RUNNING new ThreadPoolExecutor 就是 RUNNING 状态
- SHUTDOWN 不接受新的,已有继续的执行
- STOP 立即中断,全部停止
- TIDYING 全都已经停止了
- TERMINATED 钩子函数
- 死锁。所有的执行的线程都在等待阻塞队列中的结果
- 线程泄漏。一个线程执行完却没有返回到到线程池中将会导致线程泄漏。例如一个线程抛出异常,但是线程池并没有捕获。然后线程池正常退出,线程池中的数量减一。如果重复多次,线程池最终会空,没有可用线程来执行请求。
- 资源颠簸。如果一个线程池容量非常大,那么就会浪费很多时间进行上下文的切换。过多的线程比适量的线程更容易导致饥饿问题和资源颠簸。
- 不要将等待其他线程任务的结果的任务排队。换句话说,如果一个线程在等待其他线程的结果,那么就不要放到阻塞队列中,否则可能会导致死锁。
- 对于长操作小心使用线程。可能会导致其他的线程永远等待,甚至造成资源泄漏。
- 线程池最终应该被显式关闭,否则程序在线程执行完成后可能永远不会执行
shutdown()。 - 有效的使用线程池,对于类型差异非常大的任务,应该使用不同的线程池来处理不同的类型。(异构任务分别处理)
ThreadPoolExcutor -> Worker -> Thread
在 ThreadPoolExcutor 中有个 final Worker woker = this.thread;
而这个 worker.newThread 来执行我们的 任务。
Thread 属于 GC Root, thread.getTask() 是个阻塞方法,当出现问题时,则该线程则一直阻塞,GC Root 又决定无法被回收,局部线程池 ThreadPoolExecutor 没有释放,从而导致线程越用越少,最后
在 N 核 CPU 的情况下,如果是 CPU 密集型可以是 N+1 个线程,如果是 IO 密集型,可以是 2N 个线程