Javaの线程池

来源:互联网 发布:网络监控存储 编辑:程序博客网 时间:2024/06/06 17:26

池”技术对我们来说是非常熟悉的一个概念,它的引入是为了在某些场景下提高系统某些关键节点性能,最典型的例子就是数据库连接池,JDBC是一种服务供应接口(SPI),具体的数据库连接实现类由不同厂商实现,数据库连接的建立和销毁都是很耗时耗资源的操作,为了查询数据库中某条记录,最原始的一个过程是建立连接、发送查询语句、返回查询结果、销毁连接,假如仅仅是一个很简单的查询语句,那么可能建立连接与销毁连接两个步骤就已经占所有资源时间消耗的绝大部分,如此低下的效率显然让人无法接受。针对这个过程是否能通过某些手段提高效率,于是想到的尽可能减少创建和销毁连接操作,因为连接相对于查询是无状态的,不必每次查询都重新生成销毁,我们可以把这些通道维护起来供下一次查询使用,维护这些管道的工作就交给了“池”。

线程池是为了避免每次创建线程和销毁线程所带来的性能问题,原理和连接池差不多,在线程池的编程模型下,任务的提交是交给线程池的,而不是直接交给某个线程,线程池拿到某个任务后,它就在线程池内部查找有没有空余的线程,再把任务交给内部某个空余的线程,这就是封装,记住,任务是交给线程池,一个线程同时只能执行一个任务,但可以同时向线程池中提交多个任务

线程池也是类似于数据库连接池的一种池,而仅仅是把池里的对象换成了线程。线程是为多任务而引入的概念,每个线程在任意时刻执行一个任务,假如多个任务要并发执行则要用到多线程技术。每个线程都有自己的生命周期,以创建为始销毁为末。如下图,两个线程运行阶段占整个生命周期的比重不同,运行阶段所占比重小的线程可以认为其运行效率低,反观下面一条线程则认为运行效率高。在大多数场景下都比较符合图上面的线程运行模式,例如我们常见的web服务、数据库服务等等。为了提高运行效率引入线程池,它的核心思想就是把运行阶段尽量拉长,对于每个任务的到来不是重复建立销毁线程,而是重复利用之前建立好的线程执行任务
这里写图片描述
一个线程池的属性起码包含初始化线程数量、线程数组、任务队列。初始化线程数量指线程池初始化的线程数,线程数组保存了线程池中所有线程,任务队列指添加到线程池等待处理的所有任务。如下图,线程池里有两条线程,池里线程的工作就是不断循环检测任务队列中是否有需要执行的任务,如果有则处理并移出任务队列。于是可以说线程池中的所有线程的任务就是不断检测任务队列并不断执行队列中的任务。
这里写图片描述

1、产生的原因:说起线程池的产生我们必须首先了解线程的运行机制。那么什么是线程的运行机制呢?简单来说线程是怎么进行工作的?其实在我们计算机系统中,操作系统为每一个运行的线程安排一定的cup时间去处理这个线程,而这个cup时间的专业术语叫做时间片,所以cup其实在不断的切换时间片来循环的执行线程任务,由于cup的执行效率非常快,所以感觉就像多个线程在同时执行一样。但是对于单核的cup,同一个时刻只能处理单个线程,是不可能同时执行多个线程的。只有对于多核cup才有可能同时执行多个线程。所以我们知道了线程的运行过程,其实质就是cup在不断的切换线程来执行,这种循环的操作就会产生线程的不断创建和不断销毁,这样必然会浪费cup的资源,造成系统的效率变低,所以就有了线程池的产生。

2、线程池的概念:线程池其实就是预先产生线程的一种技术,将线程优先创建好放在一个类似于池子的容器中,当cup需要使用的时候直接从线程池中拿取,就避免了不断创建和销毁线程产生的浪费了。同时在效率上有了提高,线程执行的时间上也减少了。

3、Executor框架

Executor是一套线程池管理框架,接口里只有一个方法execute,执行Runnable任务。ExecutorService接口扩展了Executor,添加了线程生命周期的管理,提供任务终止、返回任务结果等方法。AbstractExecutorService实现了ExecutorService,提供例如submit方法的默认实现逻辑。

然后到今天的主题ThreadPoolExecutor,继承了AbstractExecutorService,提供线程池的具体实现。

4、线程池涉及的类:
Executor : java中线程池的顶级接口
ExecutorService: 真正的线程池接口
ScheduledExecutorService:周期执行的任务
ThreadPoolExecutor : ExecutorService的默认实现
ScheduledThreadPoolExecutor: ScheduledExecutorService接口的实现,周期性任务的类
Executors:可以一行代码创建一些常见的线程池

5、预设的定制线程池

ThreadPoolExecutor预设了一些已经定制好的线程池,由Executors里的工厂方法创建。下面分析newSingleThreadExecutor、newFixedThreadPool、newCachedThreadPool的创建参数。

线程池分类:
newSingleThreadExecutor:单线程的线程池
newFixedThreadPool:固定大小的线程池
newCacheThreadPool:可缓存的线程池
newScheduledThreadPool:周期性执行不限大小的线程池

<1>newFixedThreadPool

public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}

newFixedThreadPool的corePoolSize和maximumPoolSize都设置为传入的固定数量,keepAliveTim设置为0。线程池创建后,线程数量将会固定不变,适合需要线程很稳定的场合

<2>newSingleThreadExecutor

public static ExecutorService newSingleThreadExecutor() {        return new FinalizableDelegatedExecutorService            (new ThreadPoolExecutor(1, 1,                                    0L, TimeUnit.MILLISECONDS,                                    new LinkedBlockingQueue<Runnable>()));    }

newSingleThreadExecutor是线程数量固定为1的newFixedThreadPool版本,保证池内的任务串行。注意到返回的是FinalizableDelegatedExecutorService,来看看源码:

static class FinalizableDelegatedExecutorService        extends DelegatedExecutorService {        FinalizableDelegatedExecutorService(ExecutorService executor) {            super(executor);        }        protected void finalize() {            super.shutdown();        }    }

FinalizableDelegatedExecutorService继承了DelegatedExecutorService,仅仅在gc时增加关闭线程池的操作,再来看看DelegatedExecutorService的源码:

   static class DelegatedExecutorService extends AbstractExecutorService {        private final ExecutorService e;        DelegatedExecutorService(ExecutorService executor) { e = executor; }        public void execute(Runnable command) { e.execute(command); }        public void shutdown() { e.shutdown(); }        public List<Runnable> shutdownNow() { return e.shutdownNow(); }        public boolean isShutdown() { return e.isShutdown(); }        public boolean isTerminated() { return e.isTerminated(); }        //...    }

代码很简单,DelegatedExecutorService包装了ExecutorService,使其只暴露出ExecutorService的方法,因此不能再配置线程池的参数。本来,线程池创建的参数是可以调整的,ThreadPoolExecutor提供了set方法。使用newSingleThreadExecutor目的是生成单线程串行的线程池,如果还能配置线程池大小,那就没意思了。

Executors还提供了unconfigurableExecutorService方法,将普通线程池包装成不可配置的线程池。如果不想线程池被不明所以的后人修改,可以调用这个方法。

<3>newCachedThreadPool

public static ExecutorService newCachedThreadPool() {        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,                                      60L, TimeUnit.SECONDS,                                      new SynchronousQueue<Runnable>());    }

newCachedThreadPool生成一个会缓存的线程池,线程数量可以从0到Integer.MAX_VALUE,超时时间为1分钟。线程池用起来的效果是:如果有空闲线程,会复用线程;如果没有空闲线程,会新建线程;如果线程空闲超过1分钟,将会被回收。

<4>newScheduledThreadPool
newScheduledThreadPool将会创建一个可定时执行任务的线程池。这个不打算在本文展开,后续会另开文章细讲。

6、ThreadPoolExecutor的介绍(理解构造方法中参数的含义)
//构造方法
public ThreadPoolExecutor(int corePoolSize,//核心池的大小
int maximumPoolSize,//线程池最大线程数
long keepAliveTime,//保持时间
TimeUnit unit,//时间单位
BlockingQueue workQueue,//任务队列
ThreadFactory threadFactory,//线程工厂
RejectedExecutionHandler handler) //异常的捕捉器

7、线程池相关的方法
isShutdown():判断线程池是否关闭
isTerminated():判断线程池中的任务是否执行完毕
shutdown() : 调用后不再接收新任务,如果里面有任务,就执行完
shutdownNow() : 调用后不再接受新任务,如果有等待任务,移出队列;有正在执行的,尝试停止之
submit() : 提交执行任务
execute() : 执行任务

8、任务提交给线程池之后的处理策略
如果当前线程池中的线程数目小于corePoolSize,则每来一个任务,就会创建执行这个任务;
如果当前线程池中的线程数目>=corePoolSize,则每来一个任务,会尝试将其添加到任务缓存队列当中
若添加成功,则该任务会等待空闲线程将其取出去执行;
若添加失败(一般来说是任务缓存队列已满,针对的是有界队列),则会尝试创建新的线程去执行这个任务;
如果当前线程池中的线程数目达到maximumPoolSize,则会采取任务拒绝策略进行处理;
如果线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。

9、阻塞队列的介绍(BlockingQueue)
简介:阻塞队列,如果BlockingQueue是空的,从BlockingQueue取东西的操作将会被阻断进入等待状态,直到BlockingQueue进了东西才会被唤醒,同样,如果BlockingQueue是满的,任何试图往里存东西的操作也会被阻断进入等待状态,直到BlockingQueue里有空间时才会被唤醒继续操作。
newCachedThreadPool的线程上限几乎等同于无限,但系统资源是有限的,任务的处理速度总有可能比不上任务的提交速度。因此,可以为ThreadPoolExecutor提供一个阻塞队列来保存因线程不足而等待的Runnable任务,这就是BlockingQueue。
分类:
ArrayBlockingQueue –> 有界队列,数组结构的阻塞队列
LinkedBlockingQueue –> 无界队列,链表结构的阻塞队列
PriorityBlockingQueue –> 优先级队列
SynchronousQueue –> 交替队列,不会存储元素的阻塞队列

newFixedThreadPool和newSingleThreadExecutor在默认情况下使用一个无界的LinkedBlockingQueue。要注意的是,如果任务一直提交,但线程池又不能及时处理,等待队列将会无限制地加长,系统资源总会有消耗殆尽的一刻。所以,推荐使用有界的等待队列,避免资源耗尽。但解决一个问题,又会带来新问题:队列填满之后,再来新任务,这个时候怎么办?后文会介绍如何处理队列饱和。

newCachedThreadPool使用的SynchronousQueue十分有趣,看名称是个队列,但它却不能存储元素。要将一个任务放进队列,必须有另一个线程去接收这个任务,一个进就有一个出,队列不会存储任何东西。因此,SynchronousQueue是一种移交机制,不能算是队列。newCachedThreadPool生成的是一个没有上限的线程池,理论上提交多少任务都可以,使用SynchronousQueue作为等待队列正合适。

10、饱和策略
当有界的等待队列满了之后,就需要用到饱和策略去处理,ThreadPoolExecutor的饱和策略通过传入RejectedExecutionHandler来实现。如果没有为构造函数传入,将会使用默认的defaultHandler。

private static final RejectedExecutionHandler defaultHandler = new AbortPolicy();public static class AbortPolicy implements RejectedExecutionHandler {       public AbortPolicy() { }       public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {           throw new RejectedExecutionException("Task " + r.toString() + " rejected from " + e.toString());       }   }

AbortPolicy是默认的实现,直接抛出一个RejectedExecutionException异常,让调用者自己处理。除此之外,还有几种饱和策略,来看一下:

public static class DiscardPolicy implements RejectedExecutionHandler {       public DiscardPolicy() { }       public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {       }   }

DiscardPolicy的rejectedExecution直接是空方法,什么也不干。如果队列满了,后续的任务都抛弃掉。

 public static class DiscardOldestPolicy implements RejectedExecutionHandler {       public DiscardOldestPolicy() { }       public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {           if (!e.isShutdown()) {               e.getQueue().poll();               e.execute(r);           }       }   }

DiscardOldestPolicy会将等待队列里最旧的任务踢走,让新任务得以执行。

public static class CallerRunsPolicy implements RejectedExecutionHandler {        public CallerRunsPolicy() { }        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {            if (!e.isShutdown()) {                r.run();            }        }    }

最后一种饱和策略是CallerRunsPolicy,它既不抛弃新任务,也不抛弃旧任务,而是直接在当前线程运行这个任务。当前线程一般就是主线程啊,让主线程运行任务,说不定就阻塞了。如果不是想清楚了整套方案,还是少用这种策略为妙。

11、RejectedExecutionHandler介绍

ThreadPoolExecutor.AbortPolicy

当添加任务出错时的策略捕获器,如果出现错误,则直接抛出异常

ThreadPoolExecutor.CallerRunsPolicy

当添加任务出错时的策略捕获器,如果出现错误,直接执行加入的任务

ThreadPoolExecutor.DiscardOldestPolicy

当添加任务出错时的策略捕获器,如果出现错误,移除第一个任务,执行加入的任务

ThreadPoolExecutor.DiscardPolicy

当添加任务出错时的策略捕获器,如果出现错误,不做处理

wait………………

0 0
原创粉丝点击