线程池
我们通过学习多线程部分可知,创建线程的方法有四种:继承Thread类、实现Runnable接口、实现Callable接口和线程池。这四种方法中最推荐的方法就是线程池了。那么为什么推荐使用线程池创建线程呢?这一问题也是面试过程中常问的点,需理解记忆。
以下为推荐使用线程池创建线程的三个原因(线程池的优点):
1.降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁带来的消耗。
2.提高响应速度。当任务到达时,任务可以不需要等待线程创建就能立即执行。
3.提高线程的可管理性。使用线程池可以统一进行线程池分配、调度和监控。
1.线程池的实现原理(面试重点)
线程池实现原理图
结合线程池实现原理图,接下来捋一下线程池的主要处理流程:
第一步:当有任务时,首先判断核心线程池中是否线程数量已达最大值,若没有则直接创建新线程执行该任务即可;若核心线程池线程数量已达最大值则判断是否有空闲线程,若有则调度该空闲线程执行该任务即可,若没有则执行第二步(核心线程池线程数量已满且没有空闲线程时执行第二步);
第二步:判断工作队列(阻塞队列BlockingQueue)是否已满,若没满则将当前任务尾插入工作队列中,等待核心线程池中有线程空闲时调度即可,若已满则执行第三步;
第三步:判断线程池中普通线程数量是否已达最大值,若没有达到最大值则创建新线程执行此任务,若已满则执行第四步;
第四步:调用拒绝策略处理当前任务。
注意:第一步第三步创建线程需获取全局锁,故第一、第三步是同步处理,第二、第四步为异步处理。
所以线程池中的线程处理任务分为以下两种:
1.在execute()方法中创建一个线程时,会让这个线程执行当前任务。
2.这个线程执行完当前任务后,会反复从BlockingQueue中获取任务来执行。
2.线程池的使用
2.1线程池的创建(面试)
我们可通过ThreadPoolExecutor类实现手工创建线程池操作:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler) {}
1.corePoolSize:表示核心线程池最大线程量。
当提交一个任务到线程池时,只要核心线程池数量没有达到最大值就可直接创建线程执行该任务。调用preStartAllCoreThreads()方法,线程池就会提前创建并启动所有核心线程,这样在有任务时就可直接调度空闲线程,节省了创建线程的时间。
2.maxinumPool:表示线程池最大线程量。
3.keepAliveTime:表示线程空闲的最长时间(也就是可设置线程空闲最长时间,超过该时间时线程销毁)。
线程池的工作线程空闲后,保存存活的时间。若任务比较多,并且每个任务执行的时间比较短,可以调大此参数来提高线程的利用率。
4.TimeUnit:存活时间单位。
5.BlockingQueue<Runnable>:保存等待执行任务的工作队列。
以下为JDK内置的四大工作队列:
(1)ArrayBlockingQueue:基于数组的有界阻塞队列,按照FIFO原则对元素进行排序。
(2)LinkedBlockingQueue:基于链表的无界阻塞队列,按照FIFO原则排列元素。吞吐量要高于ArrayBlockingQueue(因链表插入删除比数组快速)。
(3)SynchronousQueue:一个不存储元素的阻塞队列(无界队列)。该队列中每个插入元素操作均需要等待另一个线程的移除操作,否则插入操作一直处于阻塞状态。吞吐量通常高于LinkedBlockingQueue。内置线程池newCachedThreadPool(缓存线程池)就采用该阻塞队列。
(4)PriorityBlockingQueue:具有优先级的阻塞队列。
6.RejectedExecutionHandler:饱和策略。
以下为四种饱和策略:
(1)AbortPolicy:无法处理新任务抛出RejectExecutionException异常(JDK默认采用此策略)。
(2)CallerRunsPolicy:使用调用者所在线程来处理任务。
(3)DisCardOldestPolicy:丢弃队列中最旧的未处理任务,然后将当前任务添加到工作队列中。
(4)DisCardPolicy:不处理任务,将任务丢弃且不报异常。
手工创建一个线程池代码示例:
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class ThreadTest{
public static void main(String[] args) {
ExecutorService executorService=new ThreadPoolExecutor(5,10,
3000,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<>());
}
}
2.1向线程池中提交任务
向线程池中提交任务有两个方法:execute()方法和submit()方法
execute()方法用于提交不需要返回值的任务,故无法判断任务是否被线程池执行成功。
submit()方法用于提交需要返回值的任务。线程池会返回一个future类型的对象,通过该对象可判断任务是否执行成功,并且可使用future的get()方法获得返回值。get()方法会阻塞当前线程直到任务完成,而使用get(long timeout,TimeUnit unit)方法则会阻塞当前线程一段时间后立即返回,这时有可能任务没有执行完。
使用execute()方法向线程池中提交任务代码示例:
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
class MyThread implements Runnable{
@Override
public void run() {
for(int i=0;i<40;i++){
System.out.println(Thread.currentThread().getName()+"、"+i);
}
}
}
public class ThreadTest{
public static void main(String[] args) {
MyThread mt=new MyThread();
ExecutorService executorService=new ThreadPoolExecutor(5,10,
3000,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<>());
for(int i=0;i<10;i++){
threadPoolExecutor.execute(mt);
}
}
}
/**
pool-1-thread-1、0
pool-1-thread-2、0
pool-1-thread-1、1
pool-1-thread-2、1
pool-1-thread-1、2
pool-1-thread-2、2
pool-1-thread-1、3
pool-1-thread-2、3
pool-1-thread-1、4
pool-1-thread-2、4
pool-1-thread-1、5
pool-1-thread-2、5
pool-1-thread-1、6
pool-1-thread-2、6
pool-1-thread-1、7
pool-1-thread-2、7
pool-1-thread-1、0
pool-1-thread-1、1
pool-1-thread-1、2
pool-1-thread-1、3
pool-1-thread-2、0
pool-1-thread-2、1
pool-1-thread-2、2
pool-1-thread-2、3
pool-1-thread-2、4
pool-1-thread-2、5
pool-1-thread-1、4
pool-1-thread-2、6
pool-1-thread-1、5
pool-1-thread-2、7
pool-1-thread-1、6
pool-1-thread-1、7
pool-1-thread-2、0
pool-1-thread-2、1
pool-1-thread-2、2
pool-1-thread-2、3
pool-1-thread-2、4
pool-1-thread-2、5
pool-1-thread-2、6
pool-1-thread-2、7
**/
使用submit()方法向线程池中提交任务代码示例:
import java.util.concurrent.*;
class MyThread implements Callable<String>{
@Override
public String call() throws Exception {
for(int i=0;i<8;i++){
System.out.println(Thread.currentThread().getName()+"、"+i);
}
return Thread.currentThread().getName()+"执行完毕";
}
}
public class ThreadTest{
public static void main(String[] args) {
MyThread mt=new MyThread();
ExecutorService executorService=new ThreadPoolExecutor(2,
3000,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<>());
for(int i=0;i<5;i++){
Future<String> future=threadPoolExecutor.submit(mt);
try {
String str=future.get();
System.out.println(str);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
}
/**
pool-1-thread-1、0
pool-1-thread-1、1
pool-1-thread-1、2
pool-1-thread-1、3
pool-1-thread-1、4
pool-1-thread-1、5
pool-1-thread-1、6
pool-1-thread-1、7
pool-1-thread-1执行完毕
pool-1-thread-2、0
pool-1-thread-2、1
pool-1-thread-2、2
pool-1-thread-2、3
pool-1-thread-2、4
pool-1-thread-2、5
pool-1-thread-2、6
pool-1-thread-2、7
pool-1-thread-2执行完毕
pool-1-thread-1、0
pool-1-thread-1、1
pool-1-thread-1、2
pool-1-thread-1、3
pool-1-thread-1、4
pool-1-thread-1、5
pool-1-thread-1、6
pool-1-thread-1、7
pool-1-thread-1执行完毕
pool-1-thread-2、0
pool-1-thread-2、1
pool-1-thread-2、2
pool-1-thread-2、3
pool-1-thread-2、4
pool-1-thread-2、5
pool-1-thread-2、6
pool-1-thread-2、7
pool-1-thread-2执行完毕
pool-1-thread-1、0
pool-1-thread-1、1
pool-1-thread-1、2
pool-1-thread-1、3
pool-1-thread-1、4
pool-1-thread-1、5
pool-1-thread-1、6
pool-1-thread-1、7
pool-1-thread-1执行完毕
**/
通过代码我们可以看出两者方法的区别:execute()方法提交线程后由系统调度并发执行。submit()方法可通过调用future.get()获取返回值,且get()方法为阻塞方法,必须等到当前线程执行完,若不调用get()方法则线程由系统调度并发执行。
使用submit()方法向线程池中提交代码能够获取返回值,原因是因为submit()方法提交的只能是实现了Callable接口的子类。
public <T> Future<T> submit(Callable<T> task) {}
execute()方法只能接收Runnable对象,submit()方法可接收Callable对象或Runnable对象。
2.3关闭线程池
建议执行完任务后将线程池关闭,否则线程不会退出,而是一直在卡顿。关闭线程池可通过调用shutdown()方法或shutdownNow()方法。它们的原理是遍历线程池中的工作线程然后逐个调用线程的interrupt()方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是这两个方法存在以下区别:
shutdown()方法只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。
shutdownNow()方法首先将线程池的状态设置为STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表。
只要调用了这两个方法中的任意一个,isShutdown方法就会返回true.通常调用shutDown()方法关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow方法。
2.4内置的四大线程池
2.4.1固定大小线程池
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
//核心线程池线程最大数量与线程池线程最大数量相同,表示线程池大小固定
固定大小线程池简单使用:
import java.util.concurrent.*;
class MyThread implements Runnable{
@Override
public void run() {
for(int i=0;i<8;i++){
System.out.println(Thread.currentThread().getName()+"、"+i);
}
}
}
public class ThreadTest{
public static void main(String[] args) throws InterruptedException {
MyThread mt=new MyThread();
//内置固定大小线程池
ExecutorService executorService =Executors.newFixedThreadPool(4);
for(int i=0;i<5;i++) {
executorService.submit(mt);
}
executorService.shutdown();
}
}
/**
pool-1-thread-1、0
pool-1-thread-2、0
pool-1-thread-2、1
pool-1-thread-3、0
pool-1-thread-2、2
pool-1-thread-4、0
pool-1-thread-4、1
pool-1-thread-1、1
pool-1-thread-1、2
pool-1-thread-4、2
pool-1-thread-2、3
pool-1-thread-3、1
pool-1-thread-2、4
pool-1-thread-4、3
pool-1-thread-4、4
pool-1-thread-1、3
pool-1-thread-4、5
pool-1-thread-2、5
pool-1-thread-3、2
pool-1-thread-3、3
pool-1-thread-3、4
pool-1-thread-3、5
pool-1-thread-3、6
pool-1-thread-3、7
pool-1-thread-2、6
pool-1-thread-2、7
pool-1-thread-4、6
pool-1-thread-4、7
pool-1-thread-1、4
pool-1-thread-1、5
pool-1-thread-1、6
pool-1-thread-1、7
pool-1-thread-3、0
pool-1-thread-3、1
pool-1-thread-3、2
pool-1-thread-3、3
pool-1-thread-3、4
pool-1-thread-3、5
pool-1-thread-3、6
pool-1-thread-3、7
**/
固定大小线程池应用场景:适用于为了满足资源管理需求,而需要限制当前线程数量的应用。适用于负载较重的服务器。
2.4.2单线程池
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
//单线程池顾名思义就是只有一个线程,故核心线程池和线程池线程最大数量值均为1
单线程简单实现:
import java.util.concurrent.*;
class MyThread implements Runnable{
@Override
public void run() {
for(int i=0;i<8;i++){
System.out.println(Thread.currentThread().getName()+"、"+i);
}
}
}
public class ThreadTest{
public static void main(String[] args) throws InterruptedException {
MyThread mt=new MyThread();
//单线程池
ExecutorService executorService =Executors.newSingleThreadExecutor();
for(int i=0;i<5;i++) {
executorService.submit(mt);
}
executorService.shutdown();
}
}
/**
pool-1-thread-1、0
pool-1-thread-1、1
pool-1-thread-1、2
pool-1-thread-1、3
pool-1-thread-1、4
pool-1-thread-1、5
pool-1-thread-1、6
pool-1-thread-1、7
pool-1-thread-1、0
pool-1-thread-1、1
pool-1-thread-1、2
pool-1-thread-1、3
pool-1-thread-1、4
pool-1-thread-1、5
pool-1-thread-1、6
pool-1-thread-1、7
pool-1-thread-1、0
pool-1-thread-1、1
pool-1-thread-1、2
pool-1-thread-1、3
pool-1-thread-1、4
pool-1-thread-1、5
pool-1-thread-1、6
pool-1-thread-1、7
pool-1-thread-1、0
pool-1-thread-1、1
pool-1-thread-1、2
pool-1-thread-1、3
pool-1-thread-1、4
pool-1-thread-1、5
pool-1-thread-1、6
pool-1-thread-1、7
pool-1-thread-1、0
pool-1-thread-1、1
pool-1-thread-1、2
pool-1-thread-1、3
pool-1-thread-1、4
pool-1-thread-1、5
pool-1-thread-1、6
pool-1-thread-1、7
**/
单线程应用场景:适用于需要保证顺序执行各个任务,并且在任意时间不会有多个线程。
2.4.3缓存线程池----根据实际需要创建线程
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
//我们可以看到该线程池核心线程池大小为0,线程池大小为整数的最大值,这样设置的原因主要是因为该线程池工作队列使用不储存元素的无界队列
//需要注意的是采取这种工作队列时当任务提交速度快于执行速度时就会不断创建线程,可能会导致内存写满
缓存线程池的简单使用:
import java.util.concurrent.*;
class MyThread implements Runnable{
@Override
public void run() {
for(int i=0;i<3;i++){
System.out.println(Thread.currentThread().getName()+"、"+i);
}
}
}
public class ThreadTest{
public static void main(String[] args) {
MyThread mt=new MyThread();
//缓存线程池
ExecutorService executorService =Executors.newCachedThreadPool();
for(int i=0;i<5;i++) {
executorService.submit(mt);
}
executorService.shutdown();
}
}
/**
pool-1-thread-1、0
pool-1-thread-1、1
pool-1-thread-1、2
pool-1-thread-2、0
pool-1-thread-2、1
pool-1-thread-2、2
pool-1-thread-3、0
pool-1-thread-3、1
pool-1-thread-3、2
pool-1-thread-4、0
pool-1-thread-4、1
pool-1-thread-4、2
pool-1-thread-5、0
pool-1-thread-5、1
pool-1-thread-5、2
**/
通过程序输出结果我们可以看出,五个任务该线程创建了五个线程执行,原因就是此时任务提交速度快于线程执行速度。
那么对以上进行简单修改:
import java.util.concurrent.*;
class MyThread implements Runnable{
@Override
public void run() {
for(int i=0;i<3;i++){
System.out.println(Thread.currentThread().getName()+"、"+i);
}
}
}
public class ThreadTest{
public static void main(String[] args) throws InterruptedException {
MyThread mt=new MyThread();
//缓存线程池
ExecutorService executorService =Executors.newCachedThreadPool();
for(int i=0;i<5;i++) {
Thread.sleep(1000);//减缓提交任务速度
executorService.submit(mt);
}
executorService.shutdown();
}
}
/**
pool-1-thread-1、0
pool-1-thread-1、1
pool-1-thread-1、2
pool-1-thread-1、0
pool-1-thread-1、1
pool-1-thread-1、2
pool-1-thread-1、0
pool-1-thread-1、1
pool-1-thread-1、2
pool-1-thread-1、0
pool-1-thread-1、1
pool-1-thread-1、2
pool-1-thread-1、0
pool-1-thread-1、1
pool-1-thread-1、2
**/
通过减缓任务提交速度,我们可以看出五个任务只有一个线程在执行,原因就是线程执行任务速度要快于任务提交速度。那么如果能合理控制任务提交速度,缓存线程池就能根据实际情况创建相应线程数量。
缓存线程池应用场景:使用于执行很多的短期异步小程序(高并发、执行速度快、访问量高),或者负载较轻的服务器。
2.4.4定时调度池
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
定时调度池的简单使用:
import java.util.concurrent.*;
class MyThread implements Runnable{
@Override
public void run() {
for(int i=0;i<3;i++){
System.out.println(Thread.currentThread().getName()+"、"+i);
}
}
}
public class ThreadTest{
public static void main(String[] args) {
MyThread mt=new MyThread();
//定时调度池
ScheduledExecutorService scheduledExecutorService=Executors.newScheduledThreadPool(4);
//定时调度池不能再用普通调度池接收,一定要使用定时调度池
for(int i=0;i<5;i++) {
//等待两秒执行
scheduledExecutorService.schedule(mt,2000,TimeUnit.MILLISECONDS);
}
scheduledExecutorService.shutdown();
}
}
/**
pool-1-thread-2、0
pool-1-thread-1、0
pool-1-thread-4、0
pool-1-thread-3、0
pool-1-thread-4、1
pool-1-thread-1、1
pool-1-thread-2、1
pool-1-thread-1、2
pool-1-thread-4、2
pool-1-thread-3、1
pool-1-thread-4、0
pool-1-thread-2、2
pool-1-thread-4、1
pool-1-thread-3、2
pool-1-thread-4、2
**/
另外一种定时调度池使用,循环实现,类似于闹钟:
import java.sql.Time;
import java.util.concurrent.*;
class MyThread implements Runnable{
@Override
public void run() {
for(int i=0;i<3;i++){
System.out.println(Thread.currentThread().getName()+"、"+i);
}
}
}
public class ThreadTest{
public static void main(String[] args) {
MyThread mt=new MyThread();
//定时调度池
ScheduledExecutorService scheduledExecutorService=Executors.newScheduledThreadPool(4);
for(int i=0;i<5;i++) {
scheduledExecutorService.scheduleAtFixedRate(mt,2000,3000,TimeUnit.MILLISECONDS);
//延迟两秒执行任务,且每隔三秒就执行一次,永远不会停止
}
}
}
定时调度池应用场景:适用于需要定时执行任务的应用场景。
若采用无界的线程池,创建线程池构造方法的哪些参数就无意义了?
(1)maximumPoolSize,原因是线程池无界,最大线程池值无意义
(2)keepAliveTime,无界线程池中我们可认为其中存在无数个线程,不断地在执行任务,线程存活时间无意义
(3)RejectedExecutionHandler,因线程池无界,只会执行第一第二步,线程池线程数量不会达到最大值,故不会出现饱和现象。