1:线程池优势
(1)降低系统资源消耗,通过重用已存在的线程,降低线程创建和销毁造成的消耗;
(2)方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的,需要保持当前执行线程的现场,并恢复要执行线程的现场)。
如何保证线程池是线程安全的?
在线程池类中通过一个互斥锁(pthread_mutex_t类型变量)来实现线程池的线程安全性。每次往任务队列中添加任务、或者从任务队列中取任务前都要使用pthread_mutex_lock函数加一把锁,然后对任务队列进行操作。操作完后再使用pthread_mutex_unlock释放这个锁,从而实现对任务队列的互斥访问。也就是说每次想要对任务队列进行操作都需要:
pthread_mutex_lock(&mutex);
增加任务到任务队列或者从任务队列取任务;
pthread_mutex_unlock(&mutex);
2:线程池ThreadPoolExecutor参数
3:线程池执行流程
4:参数详解
代码如下:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
corePoolSize 核心线程池大小
线程池的基本大小,当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,则会创建一个新的线程来执行该任务。当工作队列满了,则会创建超过corePoolSize大小的线程。注意:刚创建ThreadPoolExecutor的时候,线程不会立即启动,而是要等到有任务提交时才会启动。除非调用了prestartCoreThread/prestartAllCoreThreads事先启动核心线程。另一方面,keepAliveTime 和 allowCoreThreadTimeOut 超时参数的影响,所以没有任务需要执行的时候,线程池的大小不一定是 corePoolSize。
maximumPoolSize 最大线程池大小
线程池所允许的最大线程个数。当工作队列满了且已经创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。注意largestPoolSize,代表线程池在整个生命周期中曾经出现的最大的线程个数。
poolSize 当前线程池大小
线程池中当前线程的数量,当该值为0的时候,意味着没有任何线程,线程池会终止;同一时刻,poolSize 不会超过 maximumPoolSize。
keepAliveTime 线程最大空闲时间
是线程池中空闲线程等待工作的超时时间。单位是纳秒,1秒等于10亿纳秒。当线程池中线程数量大于corePoolSize或设置了allowCoreThreadTimeOut时,线程会根据keepAliveTime的值进行活性检查,一旦超时,便销毁线程。否则,线程会永远等待新的工作。
unit 参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性
TimeUnit.DAYS; //天
TimeUnit.HOURS; //小时
TimeUnit.MINUTES; //分钟
TimeUnit.SECONDS; //秒
TimeUnit.MILLISECONDS; //毫秒
TimeUnit.MICROSECONDS; //微妙
TimeUnit.NANOSECONDS; //纳秒
workQueue 任务队列
用于传输和保存等待执行任务的阻塞队列,包括以下四个阻塞队列:
(1)ArrayBlockingQueue:基于数组结构的有界阻塞队列,此队列按照FIFO原则对元素进行排序。此队列创建时必须指定大小。
(2)LinkedBlockingQueue:基于链表结构的阻塞队列,按照FIFO(先进先出)原则排序,吞吐量高于ArrayBlockingQueue。如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE。静态工厂方法Executors.newFixedThreadPool()使用了该队列。
(3)SynchronousQueue:不存储元素的阻塞队列。每个插入操作必须等待另一个线程调用移除操作,否则插入操作一直处理阻塞状态。吞吐量高于LinkedBlockingQueue。静态工厂方法Executors.newCachedThreadPool()使用了此队列。
(4)PriorityBlockingQueue:具有优先级的无限阻塞队列。
为什么此处的线程池要用阻塞队列呢?
我们知道队列是先进先出的。当放入一个元素的时候,会放在队列的末尾,取出元素的时候,会从队头取。那么,当队列为空或者队列满的时候怎么办呢。
这时,阻塞队列,会自动帮我们处理这种情况。
当阻塞队列为空的时候,从队列中取元素的操作就会被阻塞。当阻塞队列满的时候,往队列中放入元素的操作就会被阻塞。
而后,一旦空队列有数据了,或者满队列有空余位置时,被阻塞的线程就会被自动唤醒。
这就是阻塞队列的好处,你不需要关心线程何时被阻塞,也不需要关心线程何时被唤醒,一切都由阻塞队列自动帮我们完成。我们只需要关注具体的业务逻辑就可以了。
而这种阻塞队列经常用在生产者消费者模式中。
threadFactory 线程工厂
如果这里没有传参的话,会使用 Executors 中的默认线程工厂类 DefaultThreadFactory。
/**
* The default thread factory
*/
static class DefaultThreadFactory 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;
DefaultThreadFactory() {
SecurityManager s = System.getSecurityManager();
group = (s != null) ? s.getThreadGroup() :
Thread.currentThread().getThreadGroup();
namePrefix = "pool-" +
poolNumber.getAndIncrement() +
"-thread-";
}
public Thread newThread(Runnable r) {
Thread t = new Thread(group, r,
namePrefix + threadNumber.getAndIncrement(),
0);
if (t.isDaemon())
t.setDaemon(false);
if (t.getPriority() != Thread.NORM_PRIORITY)
t.setPriority(Thread.NORM_PRIORITY);
return t;
}
}
threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。
handler 拒绝策略
拒绝任务是指当线程池数量达到maxmumPoolSize且workQueue队列已满的情况下拒绝被尝试添加进来的任务。而handler提供了四种拒绝策略:
(1)CallerRunsPolicy:这个策略重试添加当前的任务,会自动重复调用 execute() 方法,直到成功。
(2)AbortPolicy :对拒绝任务抛弃处理,并且抛出异常。
(3)DiscardPolicy :对拒绝任务直接无声抛弃,没有异常信息。
(4)DiscardOldestPolicy :对拒绝任务不抛弃,而是抛弃队列里面等待最久的一个线程,然后把拒绝任务加到队列。
举个例子
corePoolSize:1
mamximumPoolSize:3
keepAliveTime:60s
workQueue:ArrayBlockingQueue,有界阻塞队列,队列大小是4
handler:默认的策略,抛出来一个ThreadPoolRejectException
1.一开始有一个线程变量poolSize维护当前线程数量,此时poolSize=0
2.此时来了一个任务,需要创建线程.poolSize(0) < corePoolSize(1),那么直接创建线程
3.此时来了一个任务,需要创建线程.poolSize(1) >= corePoolSize(1),此时队列没满,那么就丢到队列中去
4.如果队列也满了,但是poolSize < mamximumPoolSize,那么继续创建线程
5.如果poolSize == maximumPoolSize,那么此时再提交一个一个任务,就要执行handler,默认就是抛出异常
6.此时线程池有3个线程(poolSize == maximumPoolSize(3))。假如都处于空闲状态,但是corePoolSize=1,那么就有(3-1 =2)。那么这超出的2个空闲线程,空闲超过60s,就会给回收掉。
5:execute和submit方法区别
(1)execute(),执行一个任务,没有返回值。
(2)submit(),提交一个线程任务,有返回值。
submit(Callable task)能获取到它的返回值,通过future.get()获取(阻塞直到任务执行完)。一般使用FutureTask+Callable配合使用。
submit(Runnable task, T result)能通过传入的载体result间接获得线程的返回值。
submit(Runnable task)则是没有返回值的,就算获取它的返回值也是null。
Future.get方法会使取结果的线程进入阻塞状态,知道线程执行完成之后,唤醒取结果的线程,然后返回结果。可以参考我的另一篇博客(Java获取多线程返回值方式)
6:如何配置线程池
CPU密集型任务
尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。
IO密集型任务
可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。
如果一个项目中调用RPC方式(比如Dubbo)较多,则核心线程数要设置为2*CPU核数,因为RPC属于IO密集型任务,需要大量用到网络IO传输。
我们公司BPM系统,2台应用服务器,每台核数16,内存64G,则如果IO密级型,则核心线程池数设置为32。
7:常见线程池
7.1 newSingleThreadExecutor
单个线程的线程池,即线程池中每次只有一个线程工作。
具体执行步骤:
(1)线程池中没有线程时,新建一个线程执行任务;
(2)有一个线程以后,将任务加入阻塞队列,不停加加加;
(3)唯一的这一个线程不停地去队列里取任务执行。
单线程串行执行任务。核心线程数和最大线程数大小一样且都是1,keepAliveTime为0,阻塞队列是LinkedBlockingQueue。
SingleThreadExecutor适用于串行执行任务的场景,每个任务必须按顺序执行,不需要并发执行。
7.2 newFixedThreadPool(n)
固定数量的线程池,keepAliveTime为0,阻塞队列是LinkedBlockingQueue,每提交一个任务就是一个线程,直到达到线程池的最大数量,然后后面进入等待队列直到前面的任务完成,才去队列中取任务执行。
FixedThreadPool 适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程即可。一般n*cpu+1。
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
为什么FixedThreadExecutor的corePoolSize和mamximumPoolSize要设计成一样的?
因为线程池是先判断corePoolSize,再判断workQueue,最后判断mamximumPoolSize,然而LinkedBlockingQueue是无界队列,所以他是达不到判断mamximumPoolSize这一步的,所以mamximumPoolSize成多少,并没有多大所谓!
7.3 newCachedThreadPool(推荐使用)
可缓存线程池,当线程池大小超过了处理任务所需的线程,那么就会回收部分空闲(一般是60秒无执行)的线程,当有任务来时,又智能的添加新线程来执行。
核心线程数为0,且最大线程数为Integer.MAX_VALUE。阻塞队列是SynchronousQueue
SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue
具体执行步骤:
(1)当没有核心线程,直接向SynchronousQueue中提交任务;
(2)如果有空闲线程,就去取出任务执行;
(3)如果没有空闲线程,就新建一个执行完任务的线程有60秒生存时间,如果在这个时间内可以接到新任务,就可以继续活下去,否则就GG。
newCachedThreadPool适用于并发执行大量短期的小任务。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
为什么CachedThreadExecutor的mamximumPoolSize要设计成接近无限大的?
这个SynchronousQueue队列的容量是很小的,如果mamximumPoolSize不设计得很大,那么就很容易抛出异常
7.4 newScheduleThreadExecutor
大小无限制的线程池,支持定时和周期性的执行线程。
最大线程数为Integer.MAX_VALUE,阻塞队列是DelayedWorkQueue。
ScheduledThreadPoolExecutor 添加任务提供了另外两个方法:
scheduleAtFixedRate() :按某种速率周期执行
scheduleWithFixedDelay():在某个延迟后执行
具体执行步骤:
(1)线程从 DelayQueue 中获取 time 大于等于当前时间的 ScheduledFutureTask
DelayQueue.take()
(2)执行完后修改这个 task 的 time 为下次被执行的时间
(3)然后再把这个 task 放回队列中
DelayQueue.add()
ScheduledThreadPoolExecutor用于需要多个后台线程执行周期任务,同时需要限制线程数量的场景。
8:使用示例
案例解析 LinkedBlockingQueue:
(1)LinkedBlockingQueue,当超过核心线程数时,会将新的线程放入到linked队列里面。复用核心线程(可以看出线程名称相同!比如3个核心线程数,则超过3个后会复用这3个线程)
package com.test;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.apache.tomcat.util.collections.SynchronizedQueue;
public class ThreadPoolLinkedBlockingQueueTest {
public static void main(String[] args) throws Exception {
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" run ");
}
};
/*
LinkedBlockingQueue当线程不超过核心线程数之时,会复用核心线程数(可以看出线程名称相同!)
*/
ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 4, 5, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<>());
executor.execute(runnable);
executor.execute(runnable);
executor.execute(runnable);
System.out.println("先开3个线程-----------");
System.out.println("线程池核心线程数:"+executor.getCorePoolSize());
System.out.println("线程池中线程数:"+executor.getPoolSize());
System.out.println("线程池中队列任务数:"+executor.getQueue().size());
executor.execute(runnable);
executor.execute(runnable);
executor.execute(runnable);
System.out.println("再开3个线程-----------");
System.out.println("线程池核心线程数:"+executor.getCorePoolSize());
System.out.println("线程池中线程数:"+executor.getPoolSize());
System.out.println("线程池中队列任务数:"+executor.getQueue().size());
executor.shutdown();
}
}
(2)从下图可以看出,LinkedBlockingQueue不受最大线程数影响,但是当其queue有大小限制则会受影响!
(3) 当Queue有大小限制时,则LinkedBlockingQueue会受到限制,超过队列限定值,会报错 !
案例解析 SynchronousQueue:
package com.test;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import org.apache.tomcat.util.collections.SynchronizedQueue;
public class ThreadPoolWorkQueueTest {
public static void main(String[] args) throws Exception {
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+" run ");
}
};
ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 4, 5, TimeUnit.MILLISECONDS, new SynchronousQueue());
executor.execute(runnable);
executor.execute(runnable);
executor.execute(runnable);
System.out.println("开3个线程-----------");
System.out.println("线程池核心线程数:"+executor.getCorePoolSize());
System.out.println("线程池中线程数:"+executor.getPoolSize());
System.out.println("线程池中队列任务数:"+executor.getQueue().size());
executor.execute(runnable);
System.out.println("再开1个线程-----------");
System.out.println("线程池核心线程数:"+executor.getCorePoolSize());
System.out.println("线程池中线程数:"+executor.getPoolSize());
System.out.println("线程池中队列任务数:"+executor.getQueue().size());
/*
executor.execute(runnable);
System.out.println("再开1个线程-----------");
System.out.println("线程池核心线程数:"+executor.getCorePoolSize());
System.out.println("线程池中线程数:"+executor.getPoolSize());
System.out.println("线程池中队列任务数:"+executor.getQueue().size());
*/
Thread.sleep(8000);
System.out.println("8秒之后的线程-----------");
System.out.println("线程池核心线程数:"+executor.getCorePoolSize());
System.out.println("线程池中线程数:"+executor.getPoolSize());
System.out.println("线程池中队列任务数:"+executor.getQueue().size());
executor.shutdown();
}
}
package com.test.thread;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class TestThreadPool1 {
public static void main(String[] args) {
/*核心线程池数量大小为5
*最大线程池数量大小为10
*线程最大空闲时间200毫秒
*基于数组结构的有界阻塞队列
* 当线程池中线程的数目大于5时,便将任务放入任务缓存队列里面,当任务缓存队列满了之后,便创建新的线程。
* 如果将for循环中改成执行20个任务,就会抛出任务拒绝异常了。
*/
ThreadPoolExecutor executor = new ThreadPoolExecutor(5, 10, 200, TimeUnit.MILLISECONDS,new ArrayBlockingQueue<Runnable>(5));
//for(int i=0;i<20;i++){
for(int i=0;i<15;i++){
MyTask myTask = new MyTask(i);
executor.execute(myTask);//执行一个任务,没有返回值。
System.out.println("线程池中线程数目:"+executor.getPoolSize()+",队列中等待执行的任务数目:"+
executor.getQueue().size()+",已执行完别的任务数目:"+executor.getCompletedTaskCount());
}
executor.shutdown();
}
}
class MyTask implements Runnable {
private int taskNum;
public MyTask(int num) {
this.taskNum = num;
}
@Override
public void run() {
System.out.println("正在执行task "+taskNum);
try {
Thread.currentThread().sleep(4000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("task "+taskNum+"执行完毕");
}
}
for循环中为15时,执行结果:
for循环中为20时,执行结果:
参考文章:
Java 线程池
Java线程池详解
线程池原理
java多线程9:线程池(ExecutorService)
Java-线程池专题 (美团)
关于线程池的面试题