学习内容来源于蚂蚁课堂,感谢蚂蚁课堂大佬的指导,本文仅供学习记录使用,如有不适,请联系作者。
一、合理使用线程池的好处
- 降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
- 提高响应速度,当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性,线程是稀缺资源,如果无限制的创建,不进造成系统资源浪费,还会降低系统的稳定性,使用线程池可以统一的进行分配,调优和监控。
二、创建线程池的四种方式
2.1 创建可缓存的线程池CachedThreadPool
CachedThreadPool是大小无界的线程池,适用于执行很多的短期异步任务的小程序,或者是负载较轻的服务器。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Demo01 {
public static void main(String[] args) {
// 可缓存的线程池,可以重复利用
ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
int temp = i;
newCachedThreadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println("threadName:" + Thread.currentThread().getName() + ",i:" + temp);
}
});
}
}
}
输出:
2.2 固定线程数的线程池FixedThreadPool
FixedThreadPool适用于为了满足资源管理的需求,而需要限制当前线程数量的应用场景,它适用于负载比较重的服务器。
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Demo02 {
public static void main(String[] args) {
ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(3);
for (int i = 0; i < 10; i++) {
int temp = i;
newFixedThreadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println("threadName:" + Thread.currentThread().getName() + ",i:" + temp);
}
});
}
}
}
从输出看,最多只创建了三个线程(可以增加for循环数验证):
threadName:pool-1-thread-1,i:0
threadName:pool-1-thread-1,i:3
threadName:pool-1-thread-1,i:4
threadName:pool-1-thread-1,i:5
threadName:pool-1-thread-1,i:6
threadName:pool-1-thread-1,i:7
threadName:pool-1-thread-2,i:1
threadName:pool-1-thread-2,i:9
threadName:pool-1-thread-3,i:2
threadName:pool-1-thread-1,i:8
2.3 可定时线程池ScheduledThreadPoolExecutor
适用于需要多个后台线程执行周期任务,同时为了满足资源管理的需求而需要限制后台线程的数量的应用场景
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class Demo03 {
public static void main(String[] args) {
//可定时线程池
ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(3);
for (int i = 0; i < 10; i++) {
int temp = i;
newScheduledThreadPool.schedule(new Runnable() {
@Override
public void run() {
System.out.println("threadName:" + Thread.currentThread().getName() + ",i:" + temp);
}
},3,TimeUnit.SECONDS);
}
}
}
亲测三秒后线程开始执行:
threadName:pool-1-thread-1,i:0
threadName:pool-1-thread-3,i:2
threadName:pool-1-thread-1,i:3
threadName:pool-1-thread-2,i:1
threadName:pool-1-thread-2,i:6
threadName:pool-1-thread-3,i:4
threadName:pool-1-thread-3,i:8
threadName:pool-1-thread-2,i:7
threadName:pool-1-thread-1,i:5
threadName:pool-1-thread-3,i:9
2.4 只包含一个线程的ScheduledThreadPoolExecutor
适用于需要单个后台线程执行周期任务,同时需要保证顺序地执行各个任务的应用场景。
public class Demo04 {
public static void main(String[] args) {
//
ScheduledExecutorService newSingleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor();
for (int i = 0; i < 10; i++) {
int temp = i;
newSingleThreadScheduledExecutor.schedule(new Runnable() {
@Override
public void run() {
System.out.println("threadName:" + Thread.currentThread().getName() + ",i:" + temp);
}
},3,TimeUnit.SECONDS);
}
}
}
亲测3秒回,只有一个线程开始执行:
threadName:pool-1-thread-1,i:0
threadName:pool-1-thread-1,i:1
threadName:pool-1-thread-1,i:2
threadName:pool-1-thread-1,i:3
threadName:pool-1-thread-1,i:4
threadName:pool-1-thread-1,i:5
threadName:pool-1-thread-1,i:6
threadName:pool-1-thread-1,i:7
threadName:pool-1-thread-1,i:8
threadName:pool-1-thread-1,i:9
2.5 向线程池提交任务execute()和submit()
- execute()方法用于提交不需要返回值的任务(实现Runnable接口),无法判断任务是否被线程池执行成功。
- submit()方法用于提交需要返回值的任务(实现Callable接口)。线程池会返回一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成,而使用get(longtimeout,TimeUnitunit)方法则会阻塞当前线程一段时间后立即返回,这时候有可能任务没有执行完
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
public class Demo05 {
public static void main(String[] args) {
//
ScheduledExecutorService newSingleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor();
Future<String> future = newSingleThreadScheduledExecutor.submit(new Callable<String>() {
@Override
public String call() throws Exception {
try {
System.out.println("子线程开始执行...");
//模拟执行耗时
Thread.sleep(5000);
} catch (Exception e) {
e.printStackTrace();
}
return "submit";
}
});
try {
//获取返回值,该处会阻塞直到线程执行完毕或抛出异常
future.get();
} catch (InterruptedException e) {
//处理中断异常
e.printStackTrace();
} catch (ExecutionException e) {
//处理无法执行任务异常
e.printStackTrace();
}finally {
//关闭线程池
newSingleThreadScheduledExecutor.shutdown();
}
System.out.println("主线程执行....");
}
}
运行程序,我们发现两次输出间隔5秒,也就佐证了get()方法会阻塞当前线程直到任务完成。
子线程开始执行...
主线程执行....
扩展:
Runnable和Callable的区别:
- Callable规定的方法是call(),Runnable规定的方法是run().
- Callable的任务执行后可返回值,而Runnable的任务是不能返回值得
- call方法可以抛出异常,run方法不可以
- 运行Callable任务可以拿到一个Future对象,表示异步计算的结果。它提供了检查计算是否完成的方法,以等待计算的完成,并检索计算的结果。通过Future对象可以了解任务执行情况,可取消任务的执行,还可获取执行结果。
2.6 关闭线程池shutdown()与shutdownNow()
它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法(线程被Object.wait, Thread.join和Thread.sleep三种方法之一阻塞,此时调用该线程的interrupt()方法,那么该线程将抛出一个 InterruptedException中断异常(该线程必须事先预备好处理此异常),从而提早地终结被阻塞状态)来中断线程,所以无法响应中断的任务可能永远无法终止。
但是它们存在一定的区别,shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。
只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown方法来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow方法。
2.7 线程池的监控
可以使用如下参数监控,了解线程池状态:
- taskCount:线程池需要执行的任务数量。
- completedTaskCount:线程池在运行过程中已完成的任务数量,小于或等于taskCount。
- largestPoolSize:线程池里曾经创建过的最大线程数量。通过这个数据可以知道线程池是否曾经满过。如该数值等于线程池的最大大小,则表示线程池曾经满过。
- getPoolSize:线程池的线程数量。如果线程池不销毁的话,线程池里的线程不会自动销毁,所以这个大小只增不减。
- getActiveCount:获取活动的线程数。
- 可以通过继承线程池来自定义线程池,重写线程池的beforeExecute、afterExecute和terminated方法,也可以在任务执行前、执行后和线程池关闭前执行一些代码来进行监控。
三、线程池原理分析
java中的线程池,核心使用的是构造函数是ThreadPoolExecutor,上面使用的Executors接口不过是对其的封装。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler){}
3.1 核心构造函数参数
-
corePoolSize:核心线程池大小(可以想象成初始化池大小)
-
maximumPoolSize:线程池创建的最大线程数
-
keepAliveTime:线程空闲时间
-
workQueue:任务队列(阻塞队列)
-
threadFactory:线程池创建线程使用的工厂
-
handler:线程池对拒绝任务的处理策略
6.1. CallerRunsPolicy:提交任务的线程自己去执行该任务。
6.2. AbortPolicy:默认的拒绝策略,会 throws RejectedExecutionException。
6.3. DiscardPolicy:直接丢弃任务,没有任何异常抛出。
6.4. DiscardOldestPolicy:丢弃最老的任务,其实就是把最早进入工作队列的任务丢弃,然后把新任务加入到工作队列。
ThreadPoolExecutor采取上述步骤的总体设计思路,是为了在执行execute()方法时,尽可能地避免获取全局锁(那将会是一个严重的可伸缩瓶颈)。在ThreadPoolExecutor完成预热之后(当前运行的线程数大于等于corePoolSize),几乎所有的execute()方法调用都是执行加入队列,而加入队列不需要获取全局锁。
四、合理配置线程池大小
- CPU密集型时,可以少配置线程数,大概与机器的cup核数相当,这样可以尽可能保证每个线程都在执行任务
- IO密集型时,大部分线程都阻塞,所以需要配置更多的线程数,一般可以配置cup核数的两倍。
以上为普通情况下适用,具体视公司业务情况而定,或参考其他博客专门讲解该内容。