在博客
JAVA并发-Executor任务执行框架中曾说过,Executors有4种工厂方法,即newFixedThreadPool,newCachedThreadPool,newSingleThreadExecutor,newScheduledThreadPool。
这些工厂方法其实都是创建了一些不同执行策略的ThreadPoolExecutor。
我们可以创建自定义执行策略的ThreadPoolExecutor。ThreadPoolExecutor的通用构造函数如下:
上面的构造函数中有7个参数,依次为: 线程池基本数量,线程池最大数量,线程活跃时间,线程活跃时间单位,任务等待队列,线程工厂,饱和策略。
线程池基本数量:线程池中常驻线程的数量。
线程池最大数量:允许线程池创建的线程的最大数量。
在代码中通常不会固定线程池的大小,而是通过某种配置机制来设定,或者是通过Runtime.availableProcessors来动态的设定。一般来说,同一个线程池仅仅用来执行同一种类型的任务,这样可以更好的进行策略调整。
要正确的设置线程池的大小,需要考虑CPU数量(核心数),内存大小,任务类型(计算密集型或IO密集型),以及是否需要类似JDBC连接这样的稀有资源等问题。
通常计算密集型任务,线程池的大小设定为Ncpu+1(核心数+1)。IO密集型任务,线程池的大小设定为Ncpu*(W+C)/C(W为平均IO等待时间,C为平均计算时间)。在JAVA中,可以通过Runtime.getRuntime().availableProcessors()来获得CPU的数量(即总的核心数)。有些时候,线程池中执行的任务需要某些稀有资源,这个时候:计算每个任务对该资源的需求量,然后用资源的总量除以每个任务的需求量,所得结果就是可并发的任务总量(即最大线程数量)。
线程活跃时间:如果当前线程池中的线程数量大于基本数量,并且线程空闲时间大于活跃时间,那么这些线程将被销毁。
任务队列:如果任务的到达速度超过线程池的处理速度,那么等待被执行的任务会被放到等待队列中,这是一种缓存策略。但是 如果任务持续突增,那么任务队列可能会耗尽内存资源,此时考虑使用有界队列。ThreadPoolExecutor的基本任务队列方法有3种:无界队列,有界队列,同步队列。
饱和策略:如果任务队列被占满,就会对之后到来的任务使用饱和策略(一个任务被提交到已经关闭的Executor时,也会使用饱和策略)。 常用的饱和策略有AbortPolicy,CallerRunsPolicy,DiscardPolicy,DiscardOldestPolicy四种。AbortPolicy是默认的饱和策略,该策略抛出未检查的RejectedExecutionException。CallerRunnsPolicy使用调用者线程来执行本该被线程池执行的任务。DiscardPolicy会抛弃当前任务。DiscardOldestPolicy会抛弃等待队列中的队首任务,并将当前任务添加到队列中。饱和策略的设定方式如下:
工厂方法:每当线程池需要一个新的线程时,都是通过线程工厂方法来完成的。用户可以自定义自己的工厂方法,实现ThreadFactory接口就可以。
ThreadPoolExecutor还提供了一些上下文的方法:比如beforeExecute(),afterExecute(),terminated()方法。其中:beforeExecute()在execute()之前被调用,afterExecute()在execute()之后被调用,terminated()是在线程池终止的时候被调用。下面是一个例子:
上面的线程池MyThreadPoolExecutor添加了统计任务数,总任务时间的功能。 其中每个任务的开始时间和结束时间是在同一个线程的beforeExecute()和afterExecute()方法中获得的,因此使用ThreadLocal类型的变量来记录开始时间;而任务数和任务总时间是共享变量,需要保证线程安全性,因此使用原子变量。
还可以使用信号量来控制任务的提交速度:
我们可以创建自定义执行策略的ThreadPoolExecutor。ThreadPoolExecutor的通用构造函数如下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
上面的构造函数中有7个参数,依次为: 线程池基本数量,线程池最大数量,线程活跃时间,线程活跃时间单位,任务等待队列,线程工厂,饱和策略。
线程池基本数量:线程池中常驻线程的数量。
线程池最大数量:允许线程池创建的线程的最大数量。
在代码中通常不会固定线程池的大小,而是通过某种配置机制来设定,或者是通过Runtime.availableProcessors来动态的设定。一般来说,同一个线程池仅仅用来执行同一种类型的任务,这样可以更好的进行策略调整。
要正确的设置线程池的大小,需要考虑CPU数量(核心数),内存大小,任务类型(计算密集型或IO密集型),以及是否需要类似JDBC连接这样的稀有资源等问题。
通常计算密集型任务,线程池的大小设定为Ncpu+1(核心数+1)。IO密集型任务,线程池的大小设定为Ncpu*(W+C)/C(W为平均IO等待时间,C为平均计算时间)。在JAVA中,可以通过Runtime.getRuntime().availableProcessors()来获得CPU的数量(即总的核心数)。有些时候,线程池中执行的任务需要某些稀有资源,这个时候:计算每个任务对该资源的需求量,然后用资源的总量除以每个任务的需求量,所得结果就是可并发的任务总量(即最大线程数量)。
线程活跃时间:如果当前线程池中的线程数量大于基本数量,并且线程空闲时间大于活跃时间,那么这些线程将被销毁。
任务队列:如果任务的到达速度超过线程池的处理速度,那么等待被执行的任务会被放到等待队列中,这是一种缓存策略。但是 如果任务持续突增,那么任务队列可能会耗尽内存资源,此时考虑使用有界队列。ThreadPoolExecutor的基本任务队列方法有3种:无界队列,有界队列,同步队列。
饱和策略:如果任务队列被占满,就会对之后到来的任务使用饱和策略(一个任务被提交到已经关闭的Executor时,也会使用饱和策略)。 常用的饱和策略有AbortPolicy,CallerRunsPolicy,DiscardPolicy,DiscardOldestPolicy四种。AbortPolicy是默认的饱和策略,该策略抛出未检查的RejectedExecutionException。CallerRunnsPolicy使用调用者线程来执行本该被线程池执行的任务。DiscardPolicy会抛弃当前任务。DiscardOldestPolicy会抛弃等待队列中的队首任务,并将当前任务添加到队列中。饱和策略的设定方式如下:
ExecutorService es=new ThreadPoolExecutor(3, 5, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
new ThreadPoolExecutor.CallerRunsPolicy());
工厂方法:每当线程池需要一个新的线程时,都是通过线程工厂方法来完成的。用户可以自定义自己的工厂方法,实现ThreadFactory接口就可以。
public interface ThreadFactory {
Thread newThread(Runnable r);
}
ThreadPoolExecutor还提供了一些上下文的方法:比如beforeExecute(),afterExecute(),terminated()方法。其中:beforeExecute()在execute()之前被调用,afterExecute()在execute()之后被调用,terminated()是在线程池终止的时候被调用。下面是一个例子:
class MyThreadPoolExecutor extends ThreadPoolExecutor{
public MyThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler) {
// TODO Auto-generated constructor stub
super(corePoolSize,maximumPoolSize,keepAliveTime,unit,workQueue,handler);
}
private static ThreadLocal<Long> startTime=new ThreadLocal<Long>();
private AtomicLong taskNums=new AtomicLong();
private AtomicLong taskTimes=new AtomicLong();
@Override
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
startTime.set(System.nanoTime());
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
super.afterExecute(r, t);
long time=System.nanoTime()-startTime.get();
taskNums.incrementAndGet();
taskTimes.addAndGet(time);
}
@Override
protected void terminated() {
super.terminated();
System.out.println(taskTimes.get());
System.out.println(taskNums.get());
System.out.println(taskTimes.get()/taskNums.get());
}
}
上面的线程池MyThreadPoolExecutor添加了统计任务数,总任务时间的功能。 其中每个任务的开始时间和结束时间是在同一个线程的beforeExecute()和afterExecute()方法中获得的,因此使用ThreadLocal类型的变量来记录开始时间;而任务数和任务总时间是共享变量,需要保证线程安全性,因此使用原子变量。
还可以使用信号量来控制任务的提交速度:
class MyExecutor {
private Executor executor;
private Semaphore sem;
public MyExecutor(Executor exe,int taskNums){
this.executor=exe;
this.sem=new Semaphore(taskNums);
}
public void submit(final Runnable task) throws InterruptedException{
try{
sem.acquire();
executor.execute(new Runnable() {
@Override
public void run() {
try{
task.run();
}finally{
sem.release();
}
}
});
//由于默认使用AbortPolicy策略,所以可能抛出RejectedExecutionExecution
}catch(RejectedExecutionException e){
sem.release();
}
}
}