线程池深入(li)

时间:2024-07-07 13:34:02

  java线程池。在jdk5之后为我们提供了线程池,只需要使用API,不用去考虑线程池里特殊的处理机制。jdk5线程池分好多种,固定尺寸的线程池、可变尺寸连接池等。常用的是ThreadPoolExecutor,它的构造方法如下:

public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), handler);
}

  参数说明:

  1.corePoolSize:线程池维护线程的最少数量,有可能是空闲的线程。

  2.maximunPoolSize:线程池维护线程的最大数量。

  3.keepAliveTime:线程池维护线程所允许的空闲时间。

  4.TimeUnit:程池维护线程所允许的空闲时间的单位。

  5.workQueue:线程池所使用的缓冲队列,改缓冲队列的长度决定了能够缓冲的最大数量。

  6.RejectedExecutionHandler :拒绝任务的处理方式。

拒绝任务,是指当线程池里面的线程数量达到 maximumPoolSize 且 workQueue 队列已满的情况下被尝试添加进来的任务。在 ThreadPoolExecutor 里面定义了  种 handler 策略,分别是:

    1.CallerRunsPolicy :这个策略重试添加当前的任务,他会自动重复调用 execute() 方法,直到成功。

    2.AbortPolicy :对拒绝任务抛弃处理,并且抛出异常。

    3.DiscardPolicy :对拒绝任务直接无声抛弃,没有异常信息。

    4.DiscardOldestPolicy :对拒绝任务不抛弃,而是抛弃队列里面等待最久的一个线程,然后把拒绝任务加到队列。

一个任务通过 execute(Runnable)方法被添加到线程池,任务就是一个Runnable类型的对象,任务的执行方法就是 Runnable 类型对象的run()方法。当一个任务通过 execute(Runnable) 方法欲添加到线程池时,线程池采用的策略如下:

  1.如果此时线程池中的数量小于 corePoolSize ,即使线程池中的线程都处于空闲状态,也要创建新的线程来处理被添加的任务。

  2.如果此时线程池中的数量等于 corePoolSize ,但是缓冲队列 workQueue 未满,那么任务被放入缓冲队列。

  3.如果此时线程池中的数量大于 corePoolSize ,缓冲队列 workQueue 满,并且线程池中的数量小于maximumPoolSize ,建新的线程来处理被添加的任务。

  4.如果此时线程池中的数量大于 corePoolSize ,缓冲队列 workQueue 满,并且线程池中的数量等于maximumPoolSize ,那么通过 handler 所指定的策略来处理此任务。

处理任务的优先级为:核心线程 corePoolSize 、任务队列 workQueue 、最大线程 maximumPoolSize ,如果三者都满了,使用handler 处理被拒绝的任务。当线程池中的线程数量大于 corePoolSize 时,如果某线程空闲时间超过keepAliveTime ,线程将被终止。这样,线程池可以动态的调整池中的线程数。

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit; public class ThreadExecute { private static final int corePoolSize = 2; // 线程池维护线程的最少数量
private static final int maximumPoolSize = 4; // 线程池维护线程的最大数量
private static final long keepAliveTime = 3; // 线程池维护线程所允许的空闲时间
private static final int PRODUCETASKMAXNUMBER = 10; private static void processMessageTask() {
// 创建等待队列
BlockingQueue<Runnable> bqueue = new ArrayBlockingQueue<Runnable>(2);
// 构造一个线程池{这个策略重试添加当前的任务,他会自动重复调用 execute() 方法,直到成功}
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime,
TimeUnit.SECONDS, bqueue,
new ThreadPoolExecutor.DiscardOldestPolicy()); for (int i = 1; i <= PRODUCETASKMAXNUMBER; i++) {
try {
threadPool.execute(new MyThread());
} catch (Exception e) {
System.err.println("thread pool is error, content::" + e);
}
}
} public static void main(String[] args) {
processMessageTask();
}
} // 子类不能比父类抛出更多的异常
class MyThread implements Runnable { @Override
public void run() {
while (true) {
try {
Thread.sleep(2 * 1000);
System.out.println(Thread.currentThread().getName() + "正在执行。。。");
} catch (Exception e) { }
}
}
}

上面的代码,每两秒执行一次,并且线程会一直运行。

  总结一下,Java里面线程池的*接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。下面这张图完整描述了线程池的类体系结构:

线程池深入(li)

  ExecutorService:真正的线程池接口。

  ScheduledExecutorService:能和Timer/TimerTask类似,解决那些需要任务重复执行的问题。

  ThreadPoolExecutor:ExecutorService的默认实现。

  ScheduledThreadPoolExecutor: 继承ThreadPoolExecutor的ScheduledExecutorService接口实现,周期性任务调度的类实现。

要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在Executors类里面提供了一些静态工厂,生成一些常用的线程池。

  newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

  newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

  newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

  newScheduledThreadPool:创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

  newSingleThreadExecutor:创建一个单线程的线程池。此线程池支持定时以及周期性执行任务的需求。

当然,new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds,runnableTaskQueue, handler);用构造方法创建线程池。corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列:ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序;LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法。Executors.newCachedThreadPool使用了这个队列。PriorityBlockingQueue:一个具有优先级的无限阻塞队列。maximumPoolSize(线程池最大大小):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了*的任务队列这个参数就没什么效果。ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。以下是JDK1.5提供的四种策略:AbortPolicy:直接抛出异常;CallerRunsPolicy:只用调用者所在线程来运行任务;DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务;DiscardPolicy:不处理,丢弃掉;当然也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务;keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率;TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。

最后,说说线程池在项目中的使用。

public interface ThreadPool {

    /**
* 线程池的初始化
*/
public void init(int poolSize, int watermark); /**
* 将一个可执行的对象推到线程池队列中,在有空闲线程的情况下立刻执行
*/
public boolean schedule(Runnable runnable); /**
* 关闭线程池,调用此方法将放弃所有未执行都已经在线程队列中的Action
*/
public void close();
}

定义了接口ThreadPool,并且提供了三个方法:初始化方法、执行方法、关闭方法。

public class BlockedThreadPoolExecutor extends ThreadPoolExecutor {

    // 利用Semaphore实现的带阻塞的ThreadPoolExecutor
private Semaphore semaphore = null; public BlockedThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, int watermark){
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
this.semaphore = new Semaphore(watermark); } @Override
protected void afterExecute(Runnable r, Throwable t) {
semaphore.release();
super.afterExecute(r, t); } @Override
public void execute(Runnable command) {
try {
semaphore.acquire();
super.execute(command);
} catch (InterruptedException e) {
throw new RuntimeException(e.getMessage(), e);
}
}
}

BlockedThreadPoolExecutor实现了带阻塞的ThreadPoolExecutor。

public class BlockedThreadPoolImpl implements ThreadPool {

    protected ExecutorService pool;

    @Override
public void init(int poolSize, int watermark) {
this.pool = new BlockedThreadPoolExecutor(poolSize, poolSize, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(), watermark);
} @Override
public boolean schedule(Runnable runnable) {
boolean ret = false;
try {
pool.execute(runnable);
ret = true; } catch (Throwable t) {
System.out.println(t.getMessage() + t);
}
return ret;
} @Override
public void close() {
// 禁止新的线程从入口进入
pool.shutdown();
try {
if (!pool.awaitTermination(30, TimeUnit.SECONDS)) {
// 取消当前正在运行的线程
pool.shutdownNow();
// 等待取消的线程回应
if (!pool.awaitTermination(10, TimeUnit.SECONDS)) {
System.err.println("ThreadPoolImpl did not terminate!");
}
}
} catch (InterruptedException ie) {
// 如果遇到异常,重新尝试停止线程
pool.shutdownNow();
// 中断当前线程
Thread.currentThread().interrupt();
}
}
}

BlockedThreadPoolImpl实现了接口ThreadPool,并对init、schedule、close方法进行了实现。

  为什么要使用线程池?在Java中,如果每当一个请求到达就创建一个新线程,开销是相当大的。在实际使用中,每个请求创建新线程的服务器在创建和销毁线程上花费的时间和消耗的系统资源,甚至可能要比花在实际处理实际的用户请求的时间和资源要多的多。除了创建和销毁线程的开销之外,活动的线程也需要消耗系统资源。如果在一个JVM中创建太多的线程,可能会导致系统由于过度消耗内存或者“切换过度”而导致系统资源不足。为了防止资源不足,服务器应用程序需要一些办法来限制任何给定时刻处理的请求数目,尽可能减少创建和销毁线程的次数,特别是一些资源耗费比较大的线程的创建和销毁,尽量利用已有对象来进行服务,这就是“池化资源”技术产生的原因。线程池主要用来解决线程生命周期开销问题和资源不足问题,通过对多个任务重用线程,线程创建的开销被分摊到多个任务上了,而且由于在请求到达时线程已经存在,所以消除了创建所带来的延迟。这样,就可以立即请求服务,使应用程序响应更快。另外,通过适当的调整线程池中的线程数据可以防止出现资源不足的情况。

  Executors类常用的静态方法有哪些?Executors类里面提供了一些静态工厂,生成一些常用的线程池:newSingleThreadExecutor:创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。newFixedThreadPool:创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。newCachedThreadPool:创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。

//2016-07-25 01:00:24,411 ERROR taobao.hsf -HSF-Provider HSFthread pool is full.

//2016-07-258 01:00:24,485 ERROR taobao.hsf -HSF-Provider HSF thread pool is full.

//2016-07-25 01:00:24,644 ERROR taobao.hsf -HSF-Provider HSF thread pool is full.

//2016-07-25 01:00:24,889 ERROR taobao.hsf -HSF-Provider HSF thread pool is full.

  线程池满了,该如何解决?可以看到大量的pool full,从错误可以看出是hsf provider线程被占满(HSF默认线程池数量为600),可以定位出是外部调用tripwb系统的hsf服务所致。HSF会在出现pool full的同时,打印出堆栈到 /home/admin/hsf/HSF_JStack.log。这次出的问题,就能看出是调用httpclient这块出问题了,并且给出咱们出问题的类了,Review AMapUtil.java 果然发现是调用高德地图由于httpclient 超时出现线程的阻塞,由于代码没有设置timeout,线程就一直卡在这,而一直累积直到超过600,系统就崩溃了。

  当在使用线程的场景处,当不确定线程数的时候,尽量使用线程池。因此,针对受限的资源(线程,文件,数据库链接等),在使用时需要加以限制,如使用线程池限制线程数,数据库连接池限制链接数。当代码中执行一个批量操作,由于每次执行的时间较长,因此每次执行时都创建一个新线程异步执行。但在高访问下,该代码将会导致线程数过多。合理的方式时使用一个线程池对线程加以限制,当线程池耗尽时拒绝新启线程。

  多线程的另一处使用场景,是用来处理各种socket请求。由于Runnable接口中只提供了一个不带返回值run方法,因此当任务需要返回值时就不能满足需求了,于是出现了ExecutorService,这个接口继承了Executor,对提交任务的接口进行了扩展,引入了Callable接口,该接口定义如下:

public interface Callable<V> {
V call() throws Exception;
}

同时接口将任务执行过程进行管理,分为三个状态,提交,shutdown,terminate。