我们知道CPU运行的最小单位是线程,Java中实现并发是通过多线程来完成的,利用多线程提高了对CPU资源的利用率,但是线程的创建和销毁是很消耗性能的。线程的创建伴随着虚拟机栈、本地方法栈、程序计数器等线程私有内存空间的创建,在线程销毁的时候也伴随着这些私有内存的回收,频繁的创建和销毁线程会占用大量的系统资源,而对线程的复用则可以有效的管理和协调线程的工作。
线程池主要解决的两个问题:
线程池的体系
Executor:线程池最顶端的接口,在Executor中只有一个execute方法,用于执行任务。线程的创建、调度都是由子类实现的;
ExecutorService:继承自Executor,在Executor内部实现了任务提交机制以及线程池关闭的方法;
ThreadPoolExecutor:ExecutorService的默认实现,线程池的机制大部分封装在这个类中;
ScheduledExecutorService:继承自ExecutorService,增加了定时任务;
ScheduledThreadPoolExecutor:继承自ThreadPoolExecutor,并实现了ScheduledExecutorService接口,提供定时任务处理;
ForkJoinPool:一种任务分解的线程池,需要配合ForkJoinTask来使用。
线程池的创建
JDK中为开发者提供了一个线程池的工厂类 - Executors,Executors中提供了多个静态方法,用来创建不同配置的线程池。
创建一个单线程化的线程池,它里面只有一个工作线程,所有提交的异步任务都会放到一个先进先出的队列中按顺序执行。
所有的异步任务都使用同一个线程执行,且异步任务执行的顺序就是异步任务被提交到线程池中的顺序:
创建一个可缓存线程的线程池,当线程池中线程的长度超过了需要的数量,则会回收多余的线程,若没有可用线程,就会创建新的线程。
上面的代码中一次性提交了5个异步任务,每个异步任务内部耗时操作500ms,此时线程池就会不断创建新的线程来执行异步任务:
下面修改一下代码,每次向线程池中提交异步任务的时候等待1000ms:
再次执行代码,可以看到所有的异步任务会按顺序的在同一个线程中被执行,这是因为单个异步任务执行完毕之后,线程池并不会立即销毁线程,而是将线程缓存起来,等待下一个异步任务的提交,继续使用已经创建的线程来执行后面的异步任务,这样极大的减少了系统因为线程创建和销毁的损耗。
创建一个线程数量固定、可重用的线程池。在使用newFixedThreadPool创建线程池的时候需要指定线程池中常驻线程的数量,在后续异步任务提交的时候,不管提交了多少个Runnable任务,线程池中始终只有指定数量的线程处理任务。
上面的代码中向newFixedThreadPool中一次性提交了10个任务,但是任务只会被3个线程分配执行。
newScheduledThreadPool
会创建一个可以处理定时任务的线程池,支持定时任务和周期性任务的执行。
上面的代码中创建了一个线程数量为2的定时线程池,通过scheduleAtFixedRate指定每隔500毫秒执行一次任务,并在5秒之后通过shutdown关闭定时任务。
线程池工作原理
线程池的机构:
上图是一个线程池的结构,在一个线程池中包含以下几部分:
线程池的构造方法
构造函数的参数说明:
流程解析:
当我们通过execute或者submit向线程池提交任务,线程池在接收到任务之后,有以下几种情况:
JDK中定义了4种保护策略:
禁止使用Executors
我们在使用多线程的时候,一般禁止使用线程池的工厂类进行线程池的创建,尤其是newFixedThreadPool和newCachedThreadPool这两个方法。
我们查看newFixedThreadPool方法和newSingleThreadExecutor方法创建线程池的过程:
可以看到线程池中传入了一个无界的阻塞队列,理论上可以添加无限个任务到线程池中,当我们向等待队列中塞入特别多的任务的时候,由于等待队列无限长,线程池永远不会执行任务丢弃的保护逻辑,就会导致OOM发生。
我们在看一下newCachedThreadPool的实现代码:
newCachedThreadPool在创建线程池的时候,声明的maximumPoolSize为Interger的最大值,当核心线程耗时很久的时候,线程池就会尝试创建新的线程执行任务,当内存中无法承受新线程的创建时,就会导致OOM发生。