java并发编程实践学习(8) 应用线程池

一.任务执行策略间的隐性耦合

Executor框架可以将任务的提交和执行策略解耦,但是并非能够适合所有的执行策略。有些任务需要明确指定一个执行策略:

  1. 依赖性任务。如果你提交给线程池的任务要依赖于其它的任务,你就隐式的给执行策略带来了约束。这样你就必须仔细的管理执行策略以避免活跃度的问题。
  2. 采用线程限制任务。单线程化的Executor相比于任意线程池,可以对同步作出更强的承诺。它可以保证任务不会并发的执行,允许你放宽任务代码对于线程安全的要求。可以把对象限制在任务线程中,不需要同步。
  3. 对响应时间敏感的任务。将一个长时间运行的任务提交到单线程化的Executor中,或者将多个长时间的任务提交给一个只包含少量线程的线程池中,这样会削弱由Executor管理的服务的响应性。
  4. **使用ThreadLocal任务。**ThreadLocal让每个线程可以保留一份变量的私有版本。但是只要条件允许,Executor就会随意重用这些线程。标准的Executor的实现是 :在需求不高时回收空闲的线程,在需求增加时添加新的线程,如果任务抛出了异常就会被限制在当前的任务中,在线程池中使用ThreadLocal才有意义,在线程池中不应该使用ThreadLocal传递人物间的数值。

当任务都是同类、独立的时候,线程池才会有最佳的工作表现。如果将耗时的与短期的任务混在一起,除非线程池够大,否则会有“塞车”的危险。如果提交的任务要依赖于其他任务,除非线程池很大,否则有产生死锁的危险。

一些任务有这样的特征:需要或排斥某种特定的执行策略。对其他任务具有依赖性的任务就会要求线程池足够大,来保证他所以来的任务不必排队或者不被拒绝;采用线程限制的任务需要顺序的执行。把这些写入文档方便维护。

1.线程饥饿死锁

如果线程池在一个任务中依赖其他任务的执行,就可能产生死锁。在一个线程池中,如果所有线程执行的任务都阻塞在线程池中,等待仍然处于同一工作队列的其他任务,就会发生死锁,这样的死锁被叫做线程饥饿死锁

2.耗时操作

如果任务由于过长时间周期而阻塞,即使不出现死锁线程池的响应性也会变得很差。
限定任务等待资源的时间可以缓解耗时操作带来的影响。如果等待超时,你可以把任务标示为失败,终止它或者重新放回队列。这样无论每个任务成功还是失败都能使容我向前发展。

3.定制线程池的大小

线程池合力的大小取决于未来提交任务的类型和所部署的系统特征。池的长度应该有某种配置机制提供,或者用Runtime.availableProcessors的结果动态的计算。
为了正确的定制线程池的长度,你需要理解你的计算环境、资源预算和任务的自身特性。部署系统中安装了多少个CPU?多少内存?任务主要执行的是计算、I/O还是一些混合操作?它们是否需要像JDBC Connection这样的稀缺资源?如果你有不同类别的任务,它们拥有差别很大的行为,那么应该考虑使用多个不同的线程池,这样每个线程池可以根据不同任务的工作负载进行调节。
对于计算密集型的任务,一个有N 个处理器的系统通常通过使用一个N +1个线程的线程池来获得最优的利用率(计算密集型的线程恰好在某时因为发生一个页错误或者因为其他原因而暂停,刚好有一个“额外”的线程,可以确保在这样的情况下CPU周期不会中断工作)。对于包含了I/O和其他阻塞操作的任务,不是所有的线程都会在所有的时间被调度,因此你需要一个更大的池。为了正确地设置线程池的长度,你必须估算出任务花在等待的时间与用来计算的时间的比率;这个估算值不必十分精确,而且可以通过一些监控工具获得。你还可以选择另一种方法来调节线程池的大小,在一个基准负载下,使用不同大小的线程池运行你的应用程序,并观察CPU利用率的水平。
给定下列定义:

     N = CPU的数量
     U = 目标CPU的使用率,0 ≤ Ucpu≤ 1
     W/C = 等待时间与计算时间的比率
     为保持处理器达到期望的使用率,最优的池的大小等于:
     Nthreads = N * U * ( 1 + W/C )
     可以使用Runtime来获得CPU的数目:
     int N_CPUS = Runtime.getRuntime().availableProcessors();

CPU周期并不是唯一可以使用线程池管理的资源。其他可以约束资源池大小的资源包括:内存、文件句柄、套接字句柄和数据库连接等。计算这些资源池的大小约束非常简单:首先累加出每一个任务需要的这些资源的总量,然后除以可用的总量。所得的结果是池大小的上限。
当任务需要使用池化的资源时,比如数据库连接,那么线程池的长度和资源池的长度会互相影响。如果每一个任务都需要一个数据库连接,那么线程池的大小就限制了线程池的有效大小;类似的,当线程池中的任务是线程池的唯一消费者时,那么线程池的大小反而又会限制了连接池的有效大小。

三.配置 ThreadPoolExecutor

ThreadPoolExcutor为一些Executor提供了基本的实现,这些Executor是由Executors中的工厂 newCahceThreadPool、newFixedThreadPool和newScheduledThreadExecutor返回的。 ThreadPoolExecutor是一个灵活的健壮的池实现,允许各种各样的用户定制。
如果默认的执行策略无法满足你的需求你可以通过构造函数自己创建一个ThreadPoolExecutor
最常用的ThreadPoolExecutor构造请参见这里写链接内容

1.线程的创建与销毁

核心池大小、最大池大小和存活时间共同管理着线程的创建与销毁。核心池的大小是目标的大小;线程池的实现试图维护池的大小;即使没有任务执行,池的大小也等于核心池的大小,并直到工作队列充满前,池都不会创建更多的线程。如果当前池的大小超过了核心池的大小,线程池就会终止它。最大池的大小是可同时活动的线程数的上限。如果一个线程已经闲置的时间超过了存活时间,它将成为一个被回收的候选者。
调整核心大小和存活时间可以促进归还空闲线程占有的资源,让这些资源用于更有用的工作。

  • newFixedThreadPool工厂为请求的池设置了核心池的大小和最大池的大小,而且池永远不会超时
  • newCacheThreadPool工厂将最大池的大小设置为Integer.MAX_VALUE,核心池的大小设置为0,超时设置为一分钟。这样创建了无限扩大的线程池,会在需求量减少的情况下减少线程数量
  • 其他的组合可以用显示的ThreadPoolExecutor构造函数实现。

2.管理队列任务

如果请求到来过快超过了服务器处理它们的速度就可能有资源耗尽的危险。

  • ThreadPoolExecutor允许你提供一个BlockingQueue来持有等待执行的任务。任务排队有3种基本方法:无限队列、有限队列和同步移交。
  • newFixedThreadPool和newSingleThreadExectuor默认使用的是一个无限的 LinkedBlockingQueue。如果所有的工作者线程都处于忙碌状态,任务会在队列中等候。如果任务持续快速到达,超过了它们被执行的速度,队 列也会无限制地增加。
  • 对于庞大或无限的池,可以使用SynchronousQueue,完全绕开队列,直接将任务由生产者交给工作者线程。
  • 可以用LinkedBlockingQueue或ArrayBlockingQueue按到达数学处理任务,也可以使用PriorityBlockingQueue通过优先级安排任务。
  • newCahedThreadPool提供了比定长线程池更好的队列等候性能,是很好的默认选择。
    最稳妥的策略是使用有限队列,比如ArrayBlockingQueue或有限的LinkedBlockingQueue以及 PriorityBlockingQueue。但是队列满后应该怎么处理,这时就要依靠不同的饱和策略。
    一个大队列加一个小池可以减少上下文切换,但会增加吞吐量的开销

3.饱和策略

当有界队列满后,饱和策略开始起作用ThreadPoolExecutor的饱和策略可以通过setRejectedExecutionHandler修改。JDK提供了AbortPollicy、CallerRunsPolicy、DiscardPolicy和DiscardOldestPolicy。
- 默认的AbortPollicy(中止)策略会引起抛出未检查的 RejectedExecutionException;
- DiscardPolicy(遗弃)策略会默认放弃这个任务
- DiscardOldestPolicy(遗弃最旧的)策略会遗弃最旧的,如果是优先级队列那么会遗弃优先级最高的。
- CallerRunsPolicy(调用运行者)策略不会丢弃任务抛出异常,他会把任务推回调用者线程执行

4.线程工厂

线程池需要创建线程时要通过线程工厂完成。
ThreadFactory只有唯一的方法newThread。有很多原因需要使用定制的线程池:希望指明UncaughtExceptionHandler,实例化定制的Thread类,希望修改线程池的优先级或者后台状态、或者希望给线程一个名称简化转储和入住的解释。
定制线程工厂

public class MyThreadFactory implements ThreadFactory{
    private final String poolName;

    public MyThreadFactory(String poolName){
        this.poolName = poolName;
    }

    public Thread newThread(Runnable runnable){
        return new MyAppThread(Runnable,poolName);
    }
}

5.构造后在定制ThreadPoolExecutor

ThreadPoolExecutor也可以咋在创建后通过setters修改,比如核心线程池的大小,存活时间,线程工厂和拒绝执行处理器。

四.扩展ThreadPoolExecutor

ThreadPoolExecutor是可扩展的它提供了几个钩子去“覆写”:beforeExecute、afterExecute和terminate。
执行任务的线程会调用beforeExecute和afterExecute,无论是正常的执行完返回还是抛出异常afterExecute都会被调用。如果任务完成后抛出ErrorafterExecute不会被调用。如果before抛出一个RuntimeException任务将不执行afterExecute也不会被调用。terminated会在线程池完成关闭动作后调用。

//扩展线程池以提供日志和计时功能  
public class TimingThreadPool extends ThreadPoolExecutor{  
    //需要重写配置型的构造方法  
    public TimingThreadPool(int corePoolSize, int maximumPoolSize,  long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {  
        super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);  
    }  

    //执行任务之前  
    @Override  
    protected void beforeExecute(Thread t, Runnable r) {  
        super.beforeExecute(t, r);  
        System.out.println("执行任务之前~");  
    }  
    //执行任务之后  
    @Override  
    protected void afterExecute(Runnable r, Throwable t) {  
        super.afterExecute(r, t);  
        System.out.println("执行任务之后~");  
    }  
    //执行任务完成,需要执行关闭操作才会调用这个方法  
    @Override  
    protected void terminated() {  
        super.terminated();  
        System.out.println("执行任务完成~");  
    } 
}  

5.并行递归算法

循环并行化可以应用与一些递归设计中。一种简单的情况是,每个迭代不需要来自于它所调用的结果。
把顺序递归转化为并行递归

public<T> void sequentialRecursive(List<Node<T> nodes, Collection<T> results){
    for(Node<T> n: nodes){
        results.add(n.compute());
        sequentialRecursive(n.getChildren(),results);
    }
}
public<T> void parallelrecursive(final Executor exec,List<Node<T> nodes, final Collection<T> results){
    for(final Node<T> n:nodes){
        exec.execute(new Runnable(){
            public void run(){
                results.add(n.compute());
            }
        });
        parallelRecursive(exec,n.getChildren(),results);
    }
}

等待并行的运算结果

public<T> Collection<T> getParallelResults(List<Node<T>> nodes)throws InterruptedExceptin{
    ExecutorService exec = Executors.newCachedThreadPool();
    Queue<T> resultQueue = new ConcurrentLinkedQueue<T>();
    parallelRecursive(exec,nodes,resultQueue);
    exec.shutdown();
    exec.awaitTermination(Long.MAX_VALUE,TimeUnit.SECONDS);
    return resultQueue;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章