什么是线程池?
为了避免频繁重复的创建和销毁线程,我们可以让这些线程进行复用,在线程池中,总会有活跃的线程在占用,但是线程池中也会存在没有占用的线程,这些线程处于空闲状态,当有任务的时候会从池子里面拿去一个线程来进行使用,当完成工作后,并没有销毁线程,而是将将线程放回到池子中去。
线程池主要解决两个问题:
一是当执行大量异步任务时线程池能够提供很好的性能。
二是线程池提供了一种资源限制和管理的手段,比如可以限制现成的个数,动态新增线程等。
-《Java并发编程之美》
上面内容出自《Java并发编程之美》这本书,第一个问题上面已经提到过,线程的频繁创建和销毁是很损耗性能的,但是线程池中的线程是可以复用的,可以较好的提升性能问题,线程池内部是采用了阻塞队列来维护Runnable对象。
(想自学习编程的小伙伴请搜索圈T社区,更多行业相关资讯更有行业相关免费视频教程。完全免费哦!)
原理分析
JDK为我们封装了一套操作多线程的框架Executors,帮助我们可以更好的控制线程池,Executors下提供了一些线程池的工厂方法:
- newFixedThreadPool:返回固定长度的线程池,线程池中的线程数量是固定的。
- newCacheThreadPool:该方法返回一个根据实际情况来进行调整线程数量的线程池,空余线程存活时间是60s
- newSingleThreadExecutor:该方法返回一个只有一个线程的线程池。
- newSingleThreadScheduledExecutor:该方法返回一个SchemeExecutorService对象,线程池大小为1,SchemeExecutorService接口在ThreadPoolExecutor类和 ExecutorService接口之上的扩展,在给定时间执行某任务。
- newSchemeThreadPool:该方法返回一个SchemeExecutorService对象,可指定线程池线程数量。
对于核心的线程池来说,它内部都是使用了ThreadPoolExecutor
对象来实现的,只不过内部参数信息不一样,我们先来看两个例子:nexFixedThreadPool
和newSingleThreadExecutor
如下所示:
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
由上面的线程池的创建过程可以看到它们都是ThreadPoolExecutor
的封装,接下来我们来看一下ThreadPoolExecutor
的参数说明:
参数名称 | 参数描述 |
---|---|
corePoolSize | 指定线程池线程的数量 |
maximumPoolSize | 指定线程池中线程的最大数量 |
keepAliveTime | 当线程池线程的数量超过corePoolSize的时候,多余的空闲线程存活的时间,如果超过了corePoolSize,在keepAliveTime的时间之后,销毁线程 |
unit | keepAliveTime的单位 |
workQueue | 工作队列,将被提交但尚未执行的任务缓存起来 |
threadFactory | 线程工厂,用于创建线程,不指定为默认线程工厂DefaultThreadFactory |
handler | 拒绝策略 |
其中workQueue代表的是提交但未执行的队列,它是BlockingQueue接口的对象,用于存放Runable对象,主要分为以下几种类型:
-
直接提交的队列:
SynchronousQueue
队列,它是一个没有容量的队列,前面我有对其进行讲解,当线程池进行入队offer操作的时候,本身是无容量的,所以直接返回false,并没有保存下来,而是直接提交给线程来进行执行,如果没有空余的线程则执行拒绝策略。 -
有界的任务队列:可以使用
ArrayBlockingQueue
队列,因为它内部是基于数组来进行实现的,初始化时必须指定容量参数,当使用有界任务队列时,当有任务进行提交时,线程池的线程数量小于corePoolSize则创建新的线程来执行任务,当线程池的线程数量大于corePoolSize的时候,则将提交的任务放入到队列中,当提交的任务塞满队列后,如果线程池的线程数量没有超过maximumPoolSize,则创建新的线程执行任务,如果超过了maximumPoolSize则执行拒绝策略。 -
无界的任务队列:可以使用
LinkedBlockingQueue
队列,它内部是基于链表的形式,默认队列的长度是Integer.MAX_VALUE,也可以指定队列的长度,当队列满时进行阻塞操作,当然线程池中采用的是offer方法并不会阻塞线程,当队列满时则返回false,入队成功则则返回true,当使用LinkedBlockingQueue队列时,有任务提交到线程池时,如果线程池的数量小于corePoolSize,线程池会产生新的线程来执行任务,当线程池的线程数量大于corePoolSize时,则将提交的任务放入到队列中,等待执行任务的线程执行完之后进行消费队列中的任务,若后续仍有新的任务提交,而没有空闲的线程时,它会不断往队列中入队提交的任务,直到资源耗尽。 -
优先任务队列:t有限任务队列是带有执行优先级的队列,他可以使用
PriorityBlockingQueue
队列,可以控制任务的执行先后顺序,它是一个无界队列,该队列可以根据任务自身的优先级顺序先后执行,在确保性能的同时,也能有很好的质量保证。
上面讲解了关于线程池内部都是通过ThreadPoolExecutor
来进行实现的,那么下面我以一个例子来进行源码分析:
public class ThreadPoolDemo1 {
public static void main(String[] args) {
ExecutorService executorService = new ThreadPoolExecutor(5,
10,
60L,
TimeUnit.MILLISECONDS,
new ArrayBlockingQueue<>(5), new CustomThreadFactory());
for (int i = 0; i < 15; i++) {
executorService.execute(() -> {
try {
Thread.sleep(50000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("由线程:" + Thread.currentThread().getName() + "执行任务完成");
});
}
}
}
上面定义了一个线程池,线程池初始化的corePoolSize为5,也就是线程池中线程的数量为5,最大线程maximumThreadPoolSize为10,空余的线程存活的时间是60s,使用LinkedBlockingQueue来作为阻塞队列,这里还发现我自定义了ThreadFactory
线程池工厂,这里我真是针对线程创建的时候输出线程池的名称,源码如下所示:
/**
* 自定义的线程池构造工厂
*/
public class CustomThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
public CustomThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
@Override
public Thread newThread(Runnable r) {
String name = namePrefix + threadNumber.getAndIncrement();
Thread t = new Thread(group, r,
name,
0);
System.out.println("线程池创建,线程名称为:" + name);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
代码和DefaultThreadFactory
一样,只是在newThread
新建线程的动作的时候输出了线程池的名称,方便查看线程创建的时机,上面main
方法中提交了15个任务,调用了execute
方法来进行提交任务,在分析execute
方法之前我们先了解一下线程的状态:
//假设Integer类型是32位的二进制表示。
//高3位代表线程池的状态,低29位代表的是线程池的数量
//默认是RUNNING状态,线程池的数量为0
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
//线程个数位数,表示的Integer中除去最高的3位之后剩下的位数表示线程池的个数
private static final int COUNT_BITS = Integer.SIZE - 3;
//线程池的线程的最大数量
//这里举例是32为机器,表示为00011111111111111111111111111111
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
//线程池的状态
// runState is stored in the high-order bits
//11100000000000000000000000000000
//接受新任务并且处理阻塞队列里面任务
private static final int RUNNING = -1 << COUNT_BITS;
//000000000