白话深扒JAVA线程池 —给程序员小姐姐实现了一个线程池后她竟然……
写在前边
线程池是面试高频考点,本文从线程池是什么作为入口,大白话讲解线程池执行过程,扒源码判断线程池的选择,再用两个实战将线程池创建并且选择RejectedExecutionHandler饱和拒绝策略。
线程池简介
使用背景:
- 当我们在项目中有许多代码要用到多个线程来执行的时候
- 当电脑cpu硬件资源配置本身不足以同时创建销毁多个线程
线程池出场:
- 线程池举个银行柜台存钱的例子就可以理解:线程好比想要去银行柜台办理存钱业务的人,而线程池就是银行的柜台窗口,然而一个银行不可能只有一个窗口,窗口的数量是由银行的领导决定的(这就好比线程池的maximumPoolSize参数),其中每天银行当值的业务员同样是领导决定的,所以开放的窗口也是有限的(这就好比线程池的corePoolSize参数)。
为什么用线程池而不是直接创建线程:
- 按照上边所举的例子,那么直接创建线程来执行代码就好比某人想办理银行业务,银行行长立马就安排一个业务员给他办,那么一旦办理人数多起来,业务员有限的数量是不足以为每个人进行业务办理的,这样开的银行,早晚得凉。
- 而用线程池就好比办业务人多但是柜台窗口已经满了的情况其他的客户先等待前边的业务办理完,轮到谁办理业务谁就能去办理,一个业务员就能办理多个业务,这样用有限的资源(业务员)来承接了无限的业务(顾客)。
线程池的选择与原因
JDK提供了:
- FixedThreadPool
- CachedThreadPool(弹性缓存线程池)
- SingleThreadPool(单线程线程池)
- ScheduledThreadpool(定时器线程池)
- 四种线程池,但是都不好用,具体我通过源码来分析。
FixedThreadPool
- Executors.java
-newFixedThreadPool()方法
/**
* Creates a thread pool that reuses a fixed number of threads
* operating off a shared unbounded queue. At any point, at most
* {@code nThreads} threads will be active processing tasks.
* If additional tasks are submitted when all threads are active,
* they will wait in the queue until a thread is available.
* If any thread terminates due to a failure during execution
* prior to shutdown, a new one will take its place if needed to
* execute subsequent tasks. The threads in the pool will exist
* until it is explicitly {@link ExecutorService#shutdown shutdown}.
*
* @param nThreads the number of threads in the pool
* @return the newly created thread pool
* @throws IllegalArgumentException if {@code nThreads <= 0}
*/
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
可以看到创建FixedThreadPool的方法返回的是一个以LinkedBlockingQueue()作为等待队列的线程池,于是我们跟进LinkedBlockingQueue。
- LinkedBlockingQueue.java
//这是LinkedBlockingQueue的插入方法,可以看到只要线程数少于capacity这个变量那么都可以进入队列,这时我们找到这个变量的具体数字为多少
public boolean offer(E e) {
if (e == null) throw new NullPointerException();
final AtomicInteger count = this.count;
if (count.get() == capacity)
return false;
int c = -1;
Node<E> node = new Node<E>(e);
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
if (count.get() < capacity) {
enqueue(node);
c = count.getAndIncrement();
if (c + 1 < capacity)
notFull.signal();
}
} finally {
putLock.unlock();
}
if (c == 0)
signalNotEmpty();
return c >= 0;
}
==================================================================
可以见到这个数字的最大值是int的最大值
================================================================
/** The capacity bound, or Integer.MAX_VALUE if none */
private final int capacity;
根据代码可以看出若是使用这个线程池,那么几乎是来一个线程队列就允许进入队列,这样会堆积大量请求。
SingleThreadPool
这个线程池的等待队列和FixedThreadPool一样,上小节FixedThreadPool解析。
CachedThreadPool和ScheduledThreadpool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
=========================================================================
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
}
}
这两个线程池允许创建的最大线程数是 Integer.MAX_VALUE,那么这会导致OOM。
线程池执行流程
根据图,线程池第一步会接收工作代码并在线程池中分配线程来执行代码,此时
1.若maximumPoolSize>请求数量>corePoolSize 线程池创建线程给代码调用
2.若请求数量>maximumPoolSize 那么就会将请求放进等待队列BlockingQueue
3.若请求继续在等待队列中堆积 直到请求队列已满,并且线程池中仍然没有空闲线程
这时就触发了线程池的饱和拒绝策略:RejectedExecutionHandler,默认为AbortPolicy(抛出异常)
四大饱和拒绝策略解析
1、AbortPolicy:直接抛出异常
2、CallerRunsPolicy:只用调用所在的线程运行任务
3、DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
4、DiscardPolicy:不处理,丢弃掉。
实战1:实现一个线程池
在生产中最好是自己调用ThreadPoolExecutor来创建线程池
/*创建一个线程池
*
* corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,
* 等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。
runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。 可以选择以下几个阻塞队列。
maximumPoolSize(线程池最大大小):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,4
则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。
· ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。
· RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,
那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。以下是JDK1.5提供的四种策略。
keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多
并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
· TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),秒(SECONDS),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。
* */
public class threadPoolDemo {
static int maxNumberOfThreadPool=3;
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(2,3,1L,TimeUnit.SECONDS,new ArrayBlockingQueue<Runnable>(2),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());
//这里设置成6个任务需要线程池执行
//但是线程池最大为3 且阻塞队列最大为2 所以线程池就会拒绝
for (int i = 0; i <=5 ; i++) {
System.out.println("线程\t"+i+"\t正在提交到线程池中执行");
try{
threadPoolExecutor.execute(new ThreadDemo("工作"+(i+1)));
}
catch (Exception e)
{
// threadPoolExecutor.
try {
threadPoolExecutor.awaitTermination(3,TimeUnit.SECONDS);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
e.printStackTrace();
System.out.println(Thread.currentThread().getName()+"\t"+"线程池因为没有空余线程以及阻塞队列也满了所以"+"工作"+(i+1)+"拒绝执行");
}
}
threadPoolExecutor.shutdown();
}
}
class ThreadDemo implements Runnable
{
String threadName=null;
public ThreadDemo(String threadName) {
this.threadName = threadName;
}
@Override
public void run() {
System.out.println(threadName+"被\t"+Thread.currentThread().getName()+"\t执行了");
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(threadName+"\t执行完毕");
}
}
======================================================================
运行结果:
*线程 0 正在提交到线程池中执行
线程 1 正在提交到线程池中执行
线程 2 正在提交到线程池中执行
线程 3 正在提交到线程池中执行
线程 4 正在提交到线程池中执行
工作1被 pool-1-thread-1 执行了
线程 5 正在提交到线程池中执行
工作2被 pool-1-thread-2 执行了
工作5被 pool-1-thread-3 执行了
工作2 执行完毕
工作5 执行完毕
工作1 执行完毕
工作3被 pool-1-thread-3 执行了
工作4被 pool-1-thread-2 执行了
**********************************************************
main 线程池因为没有空余线程以及阻塞队列也满了所以工作6拒绝执行
java.util.concurrent.RejectedExecutionException: Task ThreadPool.ThreadDemo@5caf905d rejected from java.util.concurrent.ThreadPoolExecutor@27716f4[Running, pool size = 3, active threads = 3, queued tasks = 2, completed tasks = 0]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
at ThreadPool.threadPoolDemo.main(threadPoolDemo.java:39)
********************************************************
工作4 执行完毕
工作3 执行完毕
从结果可以看出默认策略是抛出异常。
实战2:RejectedExecutionHandler饱和拒绝策略的选择
new ThreadPoolExecutor.DiscardPolicy()
ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor
(2,3,1L,TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(2),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardPolicy());
运行结果:
* 线程 0 正在提交到线程池中执行
线程 1 正在提交到线程池中执行
线程 2 正在提交到线程池中执行
线程 3 正在提交到线程池中执行
线程 4 正在提交到线程池中执行
工作1被 pool-1-thread-1 执行了
工作2被 pool-1-thread-2 执行了
线程 5 正在提交到线程池中执行
工作5被 pool-1-thread-3 执行了
工作1 执行完毕
工作2 执行完毕
工作3被 pool-1-thread-2 执行了
工作4被 pool-1-thread-1 执行了
工作5 执行完毕
工作4 执行完毕
工作3 执行完毕
可以看到工作6因为在线程池中没有空闲线程以及等待队列满了所以按照RejectedExecutionHandler策略被丢弃。
new ThreadPoolExecutor.DiscardOldestPolicy()
ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor
(2,3,1L,TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(2),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardOldestPolicy());
该策略是丢弃在队列种等待最久的线程并将此线程再次提交
线程 0 正在提交到线程池中执行
线程 1 正在提交到线程池中执行
线程 2 正在提交到线程池中执行
线程 3 正在提交到线程池中执行
工作1被 pool-1-thread-1 执行了
线程 4 正在提交到线程池中执行
工作2被 pool-1-thread-2 执行了
线程 5 正在提交到线程池中执行
工作5被 pool-1-thread-3 执行了
工作5 执行完毕
工作2 执行完毕
工作1 执行完毕
工作4被 pool-1-thread-3 执行了
工作6被 pool-1-thread-1 执行了
工作6 执行完毕
工作4 执行完毕
*********************************
* 线程3被丢弃了 因为选用了等待时间长就丢弃的饱和拒绝策略
* **********************************
new ThreadPoolExecutor.CallerRunsPolicy()
ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor
(2,3,1L,TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(2),
Executors.defaultThreadFactory(),
new new ThreadPoolExecutor.CallerRunsPolicy());
线程 0 正在提交到线程池中执行
线程 1 正在提交到线程池中执行
线程 2 正在提交到线程池中执行
线程 3 正在提交到线程池中执行
工作1被 pool-1-thread-1 执行了
线程 4 正在提交到线程池中执行
工作2被 pool-1-thread-2 执行了
线程 5 正在提交到线程池中执行
***************************
工作6被 main 执行了
*****************************
工作5被 pool-1-thread-3 执行了
工作6 执行完毕
工作5 执行完毕
工作2 执行完毕
工作1 执行完毕
工作3被 pool-1-thread-3 执行了
工作4被 pool-1-thread-1 执行了
工作4 执行完毕
工作3 执行完毕