首先明确,池化的意义在于缓存,创建性能开销较大的对象,比如线程池、连接池、内存池。预先在池里创建一些对象,使用时直接取,用完就归还复用,使用策略调整池中缓存对象的数量。
Java创建对象,仅是在JVM堆分块内存,但创建一个线程,却需调用os内核API,然后os要为线程分配一系列资源,成本很高,所以线程是一个重量级对象,应避免频繁创建或销毁。
既然这么麻烦,就要避免呀,所以要使用线程池!
一般池化资源,当你需要资源时,就调用申请线程方法申请资源,用完后调用释放线程方法释放资源。但JDK的线程池根本没有申请线程和释放线程的方法。
那到底该如何理解它的设计思想呢?
其实线程池的设计,采用的是生产者-消费者模式:
- 线程池的使用方是生产者
- 线程池本身是消费者
以下简化代码即可显示线程池的基本原理:
JDK线程池最核心的就是ThreadPoolExecutor,看名字,它强调的是Executor,并非一般的池化资源。
为什么都说要手动声明线程池?
虽然JDK的Executors
工具类提供的方法可快速创建线程池。
但阿里有话说:
弊端真的这么严重吗,newFixedThreadPool=OOM?
写段测试代码:
执行不久,出现OOM
Exception in thread "http-nio-30666-ClientPoller" java.lang.OutOfMemoryError: GC overhead limit exceeded
newFixedThreadPool
线程池的工作队列直接new了一个LinkedBlockingQueue
- 但其默认构造器是一个
Integer.MAX_VALUE
长度的队列,所以很快Q满
虽然使用newFixedThreadPool
可以固定工作线程数量,但任务队列几乎无界。若任务较多且执行较慢,队列就会快速积压,内存不够,易导致OOM。
newCachedThreadPool也等于OOM?
[11:30:30.487] [http-nio-30666-exec-1] [ERROR] [.a.c.c.C.[.[.[/].[dispatcherServlet]:175 ] - Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is java.lang.OutOfMemoryError: unable to create new native thread] with root cause java.lang.OutOfMemoryError: unable to create new native thread
可见OOM是因为无法创建线程,newCachedThreadPool这种线程池的最大线程数是Integer.MAX_VALUE,也可认为无上限,而其工作队列SynchronousQueue是一个没有存储空间的阻塞队列。
所以只要有请求到来,就必须找到一条工作线程处理,若当前无空闲线程就再创建一个新的。
由于我们的任务需很长时间才能执行完成,大量任务进来后会创建大量线程。而线程是需要分配一定内存空间作为线程栈的,比如1MB,因此无限创建线程必OOM
所以使用线程池,请不要抱任何侥幸,以为只是处理轻量任务,不会造成队列积压或创建大量线程!
比如某业务一旦接受到请求,就会调用外部服务,该外部服务接口正常100ms内会响应,现在TPS过百,CachedThreadPool能稳定在占用10个左右线程情况下满足需求。
可天有不测风云,该外部服务不可用了!而代码里调用该服务设置的超时又特别长, 比如1min,1min可能已经进成千上万请求,产生几千个任务,需几千个线程,没多久就因为无法再创建新线程,OOM!
所以阿里才不建议使用Executors:
- 要结合实际并发情况,评估线程池核心参数,确保其工作行为符合预期,关键的也就是设置有界工作队列和数量可控的线程数
- 永远要为自定义的线程池设置有意义名称,以便排查问题
因为当出现线程数量暴增、死锁、CPU负载高、线程执行异常等事故时,往往都需抓取线程栈。有意义的线程名称,就很重要。示例如下:
注意异常处理