1 我们为什么需要使用线程池
线程过多会带来额外的开销,其中包括创建销毁线程的开销、调度线程的开销等等,同时也降低了计算机的整体性能。线程池维护多个线程,等待、监督、管理、分配可并发执行的任务。这种做法,一方面避免了处理任务时创建销毁线程开销的代价,另一方面避免了线程数量膨胀导致的过分调度问题,保证了对内核的充分利用。线程池是一种通过“池化”思想,帮助我们管理线程而获取并发性的工具,在Java中的体现是ThreadPoolExecutor类。
2 Executors [ɪgˈzɛkjətərz] 创建四种常见线程池
2.1 newFixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
从构造方法可以看出,它创建了一个固定大小的线程池,每次提交一个任务就创建一个线程,直到线程数达到线程池的最大值nThreads。线程池的大小一旦达到最大值后,再有新的任务提交时则放入阻塞队列中,等到有线程空闲时,再从队列中取出任务继续执行。FixedThreadPool提高程序效率和节省创建线程时所耗的开销的优点。但是,在线程池空闲时,即线程池中没有可运行任务时,它不会释放工作线程,还会占用一定的系统资源。
package com.zs.thread;
public class TestVolatile {
public static void main(String[] args) {
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 5; i++) {
final int index = i;
fixedThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
System.out.println("运行时间: " + sdf.format(new Date()) + " " + index);
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
fixedThreadPool.shutdown();
}
}
例中创建了一个固定大小为3的线程池,然后在线程池提交了5个任务。在提交第4个任务时,因为线程池的大小已经达到了3并且前3个任务在运行中,所以第4个任务被放入了队列,等待有空闲的线程时再被运行。运行结果如下(注意前3个任务和后2个任务的运行时间):
2.2 newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
从构造方法可以看出,它创建了一个可缓存的线程池。当有新的任务提交时,有空闲线程则直接处理任务,没有空闲线程则创建新的线程处理任务,队列中不储存任务。线程池不对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。如果线程空闲时间超过了60秒就会被回收。在使用CachedThreadPool时,一定要注意控制任务的数量,否则,由于大量线程同时运行,很有会造成系统OOM。
package com.zs.thread;
public class TestVolatile {
public static void main(String[] args) {
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
final int index = i;
cachedThreadPool.execute(new Runnable() {
@Override
public void run() {
try {
SimpleDateFormat sdf = new SimpleDateFormat(
"HH:mm:ss");
System.out.println("运行时间: " +
sdf.format(new Date()) + " " + index);
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
cachedThreadPool.shutdown();
}
}
因为这种线程有新的任务提交,就会创建新的线程(线程池中没有空闲线程时),不需要等待,所以提交的5个任务的运行时间是一样的。
2.3 newSingleThreadExecutor
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
从构造方法可以看出,它创建了一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。如果这个线程异常结束,会有另一个取代它,保证顺序执行。单工作线程最大的特点是可保证顺序地执行各个任务,并且在任意给定的时间不会有多个线程是活动的。
package com.zs.thread;
public class TestVolatile {
public static void main(String[] args) {
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 5; i++) {
final int index = i;
singleThreadExecutor.execute(new Runnable() {
@Override
public void run() {
try {
SimpleDateFormat sdf = new SimpleDateFormat(
"HH:mm:ss");
System.out.println("运行时间: " +
sdf.format(new Date()) + " " + index);
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
singleThreadExecutor.shutdown();
}
}
因为该线程池类似于单线程执行,所以先执行完前一个任务后,再顺序执行下一个任务:
既然类似于单线程执行,那么这种线程池还有存在的必要吗?这里的单线程执行指的是线程池内部,从线程池外的角度看,主线程在提交任务到线程池时并没有阻塞,仍然是异步的。
2.4 newScheduledThreadPool
这个方法创建了一个固定大小的线程池,支持定时及周期性任务执行。
package com.zs.thread;
public class TestVolatile {
public static void main(String[] args) {
final SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
System.out.println("提交时间: " + sdf.format(new Date()));
scheduledThreadPool.schedule(new Runnable() {
@Override
public void run() {
System.out.println("运行时间: " + sdf.format(new Date()));
}
}, 3, TimeUnit.SECONDS);
scheduledThreadPool.shutdown();
}
}
使用该线程池的schedule方法,延迟3秒钟后执行任务,运行结果如下:
再看一下周期执行的例子:
package com.zs.thread;
public class TestVolatile {
public static void main(String[] args) throws InterruptedException {
final SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
System.out.println("提交时间: " + sdf.format(new Date()));
scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("运行时间: " + sdf.format(new Date()));
}
}, 1, 3, TimeUnit.SECONDS);
Thread.sleep(10000);
scheduledThreadPool.shutdown();
}
}
使用该线程池的scheduleAtFixedRate方法,延迟1秒钟后每隔3秒执行一次任务,运行结果如下:
2. 5 Executors 各个方法的弊端:
阿里巴巴的Java操作手册中明确说明:对于Integer.MAX_VALUE初始值较大,所以一般情况我们要使用底层的ThreadPoolExecutor来创建线程池。
3 生命周期管理,线程池都有哪些状态?
任务调度
首先,所有任务的调度都是由execute方法完成的:(workerCount:前线程池的线程数,corePoolSize:基本大小线程数,maximumPoolSize:线程池中允许的最大线程数)
1 首先检测线程池运行状态,如果不是RUNNING,则直接拒绝,线程池要保证在RUNNING的状态下执行任务。
2 如果workerCount < corePoolSize,则创建并启动一个线程来执行新提交的任务。
3 如果workerCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中。
4 如果workerCount >= corePoolSize && workerCount < maximumPoolSize,且线程池内的阻塞队列已满,则创建并启动一个线程来执行新提交的任务。
5 如果workerCount >= maximumPoolSize,并且线程池内的阻塞队列已满, 则根据拒绝策略来处理该任务, 默认的处理方式是直接抛异常。
任务缓冲
线程池的本质是对任务和线程的管理:将任务和线程两者解耦,才可以做后续的分配工作。线程池中是以生产者消费者模式,通过一个阻塞队列来实现的。阻塞队列缓存任务,工作线程从阻塞队列中获取任务。 阻塞队列(BlockingQueue)是一个支持两个附加操作的队列:在队列为空时,获取任务的线程会等待队列变为非空;当队列满时,存储任务的线程会等待队列可用。
使用不同的队列可以实现不一样的任务存取策略。阻塞队列的成员有以下:
任务申请
一种是:任务直接由新创建的线程执行。另一种是:线程从任务队列中获取任务然后执行,执行完任务的空闲线程会再次去从队列中申请任务再去执行。
任务拒绝
任务拒绝模块是线程池的保护部分,线程池有一个最大的容量,当线程池的任务缓存队列已满,并且线程池中的线程数目达到maximumPoolSize时,就需要拒绝掉该任务,采取任务拒绝策略,保护线程池。
拒绝策略 | 策略 |
---|---|
AbortPolicy | 丢弃任务,并抛出异常:最大承载=maximumPoolSize + BlockingQueue |
CallerRunsPolicy | 由提交任务的线程处理该任务 |
DiscardPolicy | 不处理新任务,直接丢弃掉 |
DiscardOldestPolicy | 丢弃队列最前面的任务,然后重新提交被拒绝的任务。 |
4 创建线程池的七大参数
参数 | 特点 |
---|---|
corePoolSize | 核心线程池大小:线程池中会维护一个最小的线程数量,即使这些线程处理空闲状态,他们也不会被销毁。当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时,除了利用提交新任务来创建和启动线程(按需构造),也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程。 |
maximumPoolSize | 一个任务被提交到线程池以后,如果工作队列满了,才会创建一个新线程,然后从工作队列的头部取出一个任务交由新线程来处理,而将刚提交的任务放入工作队列尾部。线程池不会无限制的去创建新线程,它会有一个最大线程数量的限制,这个数量即由maximunPoolSize指定。 |
keepAliveTime | 一个线程如果处于空闲状态,并且当前的线程数量大于corePoolSize,那么在指定时间后,这个空闲线程会被销毁;如果allowCoreThreadTimeOut被设置为true时,无论线程数多少,线程处于空闲状态超过一定时间就会被销毁掉 |
TimeUnit unit | 超时单位 |
BlockingQueue workQueue | 用来保存等待被执行的任务的阻塞队列 |
ThreadFactory threadFactory | 为线程池提供创建新线程的线程工厂 |
RejectedExecutionHandler handler | 当工作队列中的任务已到达最大限制,并且线程池中的线程数量也达到最大限制,这时如果有新任务提交进来,会执行拒绝策略 |
5 你知道怎么创建线程池吗?
ThreadPoolExecutor() 是最原始的线程池创建,也是阿里巴巴 Java 开发手册中明确规范的创建线程池的方式。
public class Test {
public static void main(String[] args) {
// 获取cpu 的核数
int max = Runtime.getRuntime().availableProcessors();
ExecutorService service =new ThreadPoolExecutor(
2,
max,
3,
TimeUnit.SECONDS,
new LinkedBlockingDeque<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
try {
for (int i = 1; i <= 10; i++) {
service.execute(() -> {
System.out.println(Thread.currentThread().getName() + "ok");
});
}
}catch (Exception e) {
e.printStackTrace();
}
finally {
service.shutdown();
}
}
}
6 ThreadPoolExecutor
Java中的线程池核心实现类是ThreadPoolExecutor。ThreadPoolExecutor是如何运行,如何同时维护线程和执行任务的呢?
线程池在内部实际上构建了一个生产者消费者模型,将线程和任务两者解耦,并不直接关联,从而良好的缓冲任务,复用线程。线程池的运行主要分成两部分:任务管理、线程管理。
任务管理部分充当生产者的角色,当任务提交后,线程池会判断该任务后续的流转:
1 直接申请线程执行该任务;
2 缓冲到队列中等待线程执行;
3 拒绝该任务。
线程管理部分是消费者,它们被统一维护在线程池内,根据任务请求进行线程的分配,当线程执行完任务后则会继续获取新的任务去执行,最终当线程获取不到任务的时候,线程就会被回收。
7 如何优雅设置线程池的大小
线程池需要设置合适的大小,假如设置的太大,线程上线文切换过于频繁,造成大量资源开销,反而会使性能降低。假如设置的太小,存在很多可用的处理器资源却未在工作,会造成资源的浪费和对吞吐量造成损失。
为了充分利用处理器资源,创建的线程数至少要等于处理器核心数。如果所有的任务都是计算密集型的,那么线程数等于可用的处理器核心数就可以了。不过,如果所有的任务都是IO密集型,那么处理器大部分时间是空闲的,所以要适当的增加线程数。线程等待时间所占比例越高(IO密集型),需要越多线程。线程运算时间所占比例越高(计算密集型),需要越少线程。 于是可以使用下面的公式进行估算:
最佳线程数 = (1 + 线程等待时间/线程计算时间)* 目标CPU的使用率 * 处理器核心数
例如:平均每个线程计算运行时间为0.5s,而线程等待时间(非计算时间,比如IO)为1.5s,目标CPU的使用率是90%,CPU核心数为8,那么根据上面这个公式估算得到:(1 + 1.5/0.5) * 90% * 8 = 28.8。
即使有上面的简单估算方法,也许看似合理,但实际上也未必合理,都需要结合系统真实情况(比如是IO密集型或者是CPU密集型或者是纯内存操作)和硬件环境(CPU、内存、硬盘读写速度、网络状况等)来不断尝试达到一个符合实际的合理估算值,也可以尝试Dark Magic的估算方法。
8 线程池为什么要使用阻塞队列而不使用非阻塞队列?
阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。当队列中有任务时才唤醒对应线程从队列中取出消息进行执行。使得在线程不至于一直占用cpu资源。
9 线程池是如何保持核心线程不被摧毁呢?
1、客户端创建线程池对象后,调用execute()
提交一个Runnable
任务;
2、execute()
会调用addWorker()
创建一个Worker
对象;
3、addWorker()
内部会调用Worker.thread.start()
这时候实际调用的就是Worker
对象内部的run
方法;
4、Worker
中的run
方法委托给runWorker()
执行;
5、runWorker()
中有while
循环体,不断地调用getTask()
获取新任务;
6、在getTask()
方法里它就是调用阻塞队列的poll()
或take()
等待获取其中的任务,getTask()
通过调用blockQueue
的take()
获取队列中的任务,如果队列为空,就一直阻塞当前线程,利用了阻塞队列的特性,实现核心线程空闲时间,也保持live;
package com.thread.pool;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;
import static java.lang.Thread.sleep;
public class TestFixedThreadPool {
public static void main(String[] args) {
ExecutorService fixedThreadPool = getThreadPoolExecutorService_bk();
fixedThreadPool.shutdown();
}
private static ExecutorService getFixedExecutorService() {
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
IntStream.rangeClosed(1, 5).forEach(
item -> fixedThreadPool.execute(() -> {
try {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
System.out.println("运行时间 - " + sdf.format(new Date()) + " " + item);
sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}));
return fixedThreadPool;
}
private static ExecutorService getCachedExecutorService() {
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
IntStream.rangeClosed(1, 10).forEach(
item -> cachedThreadPool.execute(() -> {
try {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
System.out.println("运行时间 - " + sdf.format(new Date()) + " " + item);
sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}}));
return cachedThreadPool;
}
private static ExecutorService getSingleExecutorService() {
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
IntStream.rangeClosed(1, 8).forEach(
item -> singleThreadExecutor.execute(() -> {
try {
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
System.out.println("运行时间 - " + sdf.format(new Date()) + " " + item);
sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}}));
return singleThreadExecutor;
}
private static ExecutorService getScheduledExecutorService() {
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
System.out.println("提交时间: " + sdf.format(new Date()));
scheduledThreadPool.schedule(() ->
System.out.println("运行时间: " + sdf.format(new Date())), 3, TimeUnit.SECONDS);
scheduledThreadPool.scheduleAtFixedRate(() ->
System.out.println("运行时间: " + sdf.format(new Date())), 1, 3, TimeUnit.SECONDS);
return scheduledThreadPool;
}
private static ExecutorService getThreadPoolExecutorService() {
int max = Runtime.getRuntime().availableProcessors();
System.out.println(max);
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2,
max,
3,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
IntStream.rangeClosed(1, 10).forEach(
item -> threadPoolExecutor.execute(() -> {
System.out.println(Thread.currentThread().getName() + " ok " + sdf.format(new Date()));
try {
sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
})
);
threadPoolExecutor.shutdown();
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
System.out.println(threadPoolExecutor.getPoolSize());
return threadPoolExecutor;
}
/**
* 由提交任务的线程处理该任务 不会丢弃任务
* @return
*/
private static ExecutorService getThreadPoolExecutorService_bk() {
int max = Runtime.getRuntime().availableProcessors();
System.out.println(max);
SimpleDateFormat sdf = new SimpleDateFormat("HH:mm:ss");
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2,
2,
3,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
IntStream.rangeClosed(1, 10).forEach(
item -> CompletableFuture.supplyAsync(() -> {
System.out.println(Thread.currentThread().getName() + " ok " + sdf.format(new Date()));
try {
sleep(3000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
return null;
}, threadPoolExecutor)
);
threadPoolExecutor.shutdown();
return threadPoolExecutor;
}
}