浅谈java线程池(上)
从多线程开始
计算机发展初期,还没有操作系统,它们始终运行一个程序,这个程序访问所有资源。这样运行方式使得人们不能很好的利用计算机资源。
操作系统的发展使得程序可以同时运行,也促进了线程的发展。
多线程的优点
合理利用多线程,可以让程序运行得更快,能够充分利用计算机的资源,可以简单的处理异步事件,也可以构造友好的用户界面。
多线程的风险
并不是所有多线程都能够提高程序性能,并发的风险主要体现在:
上下文切换
:CPU资源是非常宝贵的,CPU花费大量资源在对用户线程的切换上不值得。死锁
:多个线程对同一资源的互相争用,陷入死循环。资源限制
:计算机资源是有限的,内存,IO,带宽。资源的限制也会让多线程陷入麻烦。
因此,使用多线程编程也需要引入一些特定的框架和方法,以规避风险。
java中的线程池
线程池就是运用场景最多的并发框架,几乎所有用到并发或异步执行的程序都可以用到线程池。合理使用线程池能够带来:
降低资源消耗
。重复利用已经创建的线程减少创建销毁线程的消耗。提高响应速度
。免去创建线程消耗的时间。方便管理线程
。统一调配线程,避免线程无限制耗费资源。
Executor框架
Executor是一个用户级别的调度器,它用于将用户任务映射到固定数量的线程上,操作系统内核将这些线程映射到处理器上。Executor框架的作用就是将用户任务与操作系统内核隔离。这种两级调用模型图如下:
下面详细介绍一下Executor框架中的重要接口和类。
先看一下类与接口关系图
Executor接口
Executor是一个根接口,它只定义了execute
方法,参数Runnable
public interface Executor {
void execute(Runnable command);
}
ExecutorService接口
ExecutorService 接口对 Executor 接口进行了扩展,提供了终止,关闭线程池方法。重载了执行任务的方法,提供了一个返回为 Future
的泛形对象,它能够获取任务的执行结果。
线程池都需要实现这个接口,所以通常定义为线程池的通用接口。
Future接口
提供取消任务,获取任务执行结果方法,它可以这样用
public void TestFuture {
ExecutorService threadPool = Executors.newCachedThreadPool();
Future<String> future = threadPool.submit(new Callable<String>() {
@Override
public String call() throws Exception {
return "hello world";
}});
try {
// 获取任务执行结果
String result = future.get();
} catch (InterruptedException e) {
// 中断异常
} catch (ExecutionException e) {
// 执行异常
}
}
FutureTask类
Future 的实现类是FutureTask
。FutureTask 也实现了Runnable
接口,所以可以将 FutureTask 理解为一个具有返回值的 Thread。
public void TestFutureTask {
Future<String> futureTask = new FutureTask<>(new Callable<String>() {
@Override
public String call() throws Exception {
return "hello world";
}});
try {
String result = futureTask.get();
} catch (InterruptedException e) {
// 中断异常
} catch (ExecutionException e) {
// 执行异常
}
}
Executors类
Executors是一个工具类,它可以作为一个线程池工厂创建一些内置的线程池,这些线程池通常在特定场景下使用,不过在实际应用中通常都不会使用这个工具创建线程池,而是使用ThreadPoolExecutor
自己去自定义一个满足自己业务需要的线程池。
以下是 Executors 内置线程池的特性
ExecutorService | 线程池 |
---|---|
newCachedThreadPool() | 可缓存的线程池,该线程池中没有核心线程,当有需要时创建线程来执行任务,没有需要时回收线程,几乎没有设置线程数量上限。适用于耗时少,任务量大的情况。 |
newFixedThreadPool(int i) | 固定长度线程池,针对一些很稳定很固定的正规并发线程。 |
newScheduledThreadPool(int i) | 调度型线程池,可按设定时间执行,也可周期执行的任务 |
newSingleThreadExecutor() | 单例线程,用于顺序执行的任务 |
newWorkStealingPool(int i) | 返回ForJoinPool线程池,指定并行级别执行线程任务 |
ThreadPoolExecutor类
这是 Executor 框架里最重要的实现类了,先看它的使用方法。
public void TestThreadPoolExecutor {
final ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
coreThreadNum,
maxThreadNum,
liveTime,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(queueSize),
new ThreadPoolExecutor.DiscardPolicy());
// 优雅关闭线程池
Runtime.getRuntime().addShutdownHook(new Thread(){
public void run(){
try {
logger.warn("shutdown common threadPoolExecutor");
threadPool.shutdown();
} catch (Throwable e) {
logger.error(e.getMessage(), e);
}
}
});
}
解释一下构造函数的各个参数
coreThreadNum
:线程池中所保存的核心线程数,包括空闲线程。maxThreadNum
:池中允许的最大线程数。liveTime
:线程最大空闲时间。TimeUnit.SECONDS
:空闲时间单位。LinkedBlockingQueue
:任务执行前保存任务的队列,仅保存由execute方法提交的Runnable任务。policy
:饱和策略
饱和策略有四种
AbortPolicy
(终止执行):默认策略, Executor会抛出一个RejectedExecutionException运行异常到调用者线程来完成终止。CallerRunsPolicy
(调用者线程来运行任务):这种策略会由调用execute方法的线程自身来执行任务,它提供了一个简单的反馈机制并能降低新任务的提交频率。DiscardPolicy
(丢弃策略):不处理,直接丢弃提交的任务。DiscardOldestPolicy
(丢弃队列里最近的一个任务):如果Executor还未shutdown的话,则丢弃工作队列的最近的一个任务,然后执行当前任务。
小结
这篇文章重点介绍一下java的 Executor 框架,以及使用方法。
下篇文章将通过分析 Excutor 框架中重要实现的源码,来理解java设计者对线程池的设计思路。