Java线程之线程池

版权声明:未经本人允许,严禁转载 https://blog.csdn.net/lmh_19941113/article/details/85315845

Executor框架可以将任务的提交与任务的执行策略解耦开来。就像许多对复杂过程的解耦操作那样,这种论断多少有些言过其实了。虽然Executor框架为定制和修改执行策略提供了相当大的灵活性,但并非所有的任务都能适用所有的执行策略。有些类型的任务需要明确的指定执行策略。

依赖性任务 使用线程封闭机制的任务 对相应时间敏感的任务 使用ThreadLocal的任务
大多数行为正确的任务都是独立的:它们不依赖于其他任务的执行时序,执行结果或其他效果。当线程池中执行独立任务时,可以随意地改变线程池的大小和配置,这些修改只会对执行性能产生影响。然而如果提交给线程池的任务需要依赖其他任务,那么就隐含地给执行策略带来约束,此时必须小心的维持这些执行策略以避免产生活跃性问题 与线程池相比,单线程的Executor能够对并发性做出更强的承诺。它们能够确保任务不会并发的执行,使你能够放宽代码对线程安全的要求。对象可以封闭在任务线程中,使得该线程中执行的任务在访问该对象时不需要同步。即使这些资源不是线程安全的也没有问题。这种情形将在任务与执行策略之间形成隐式的耦合——任务要求其执行所在的Executor是单线程的。如果将Executor从单线程环境改变为线程池环境,那么将会失去线程安全性。 GUI应用程序对于相应时间是敏感的:如果用户在点击按钮后需要很长延迟才能得到可见的反馈,那么他们会感到不满。如果将一个运行时间较长的任务提交到单线程的Executor中,或者将多个运行时间较长的任务提交到一个只包含少量线程的线程池中,那么将降低由该Executor管理的服务的响应性 ThreadLocal使每个线程都可以拥有某个变量的一个私有“版本”。然而,只要条件允许,Executor可以自由地重用这些线程。在标准的Executor实现中,当执行需求较低时将回收空闲线程,而当需求增加时将添加新的线程,并且如果从任务中抛了一个未受检查的异常,那么将一个新的工作者线程来替代抛出异常的线程。只有当线程本地值得生命周期受限于任务的生命周期时,在线程池的线程中使用ThreadLocal才有意义,而在线程池的线程中不应该使用ThreadLocal在任务之间传值

 只有当任务都是同类型的并且相互独立时,线程池的性能才能达到最佳。如果将运行时间较长的任务与运行时间较短的任务混合在一起,那么除非线程池很大,否则有可能造成“拥塞”。如果提交的任务依赖于其他任务,那么除非线程池无限大,否则有可能造成死锁。幸运的是,在基于网络的典型服务器应用程序中——网页服务器、邮件服务器以及文件服务器等,它们的请求通常都是同类型的并且相互独立的。

在一些任务中,需要拥有或排除某种特定的执行策略。如果某些任务依赖于其他的任务,那么将会要求线程池足够大,从而确保它们依赖任务不会被放入等待队列中或被拒绝,而采用线程封闭机制的任务需要串行执行。通过将这些需求写入文档,将来的代码维护人员就不会由于使用可某种不合适的执行策略而破坏安全性或活跃性。

线程饥饿死锁

 在线程池中,如果任依赖于其他任务,那么可能产生死锁。在单线程的Executor中,如果一个任务将另一个任务提交到同一个Executor,并且等待这个被提交任务的结果,那么通常会引发死锁。第二个任务停留在工作队列中,并等待第一个任务的完成,而第一个人任务又无法完成,因为他在等待第二个任务的完成。在更大的线程池中,如果所有正在执行任务的线程都由与等待其他仍处于工作队列中的任务而阻塞,那么将会发生同样的问题。这种现象被称为线程饥饿死锁,只要线程池中的任务需要无限地等待一些必须由池中其他任务才能提供的资源或条件,例如在某个任务等待另一个任务返回值或者执行结果,那么除非线程池足够大,否则将会发生线程饥饿死锁。

每当提交了一个有依赖性的Executor任务时,要清楚地知道可能会出现线程“饥饿”死锁,因此需要在代码或配置Executor的配置文件中记录线程池的大小限制或配置限制。

 除了在线程池大小上的显示限制外,还可能由于其他资源上的约束而存在一些隐式限制。如果应用程序使用一个包含10个连接的JSBC连接池,并且每个任务需要一个数据库连接,那么线程池就好像只有10个线程,因为当超过10个任务时,新的任务需要等待其他任务释放连接。

运行时间较长的任务

 如果任务阻塞的时间过长,那么即使不出现死锁,线程池的响应性也会变得糟糕。执行时间较长的任务不仅会造成线程池堵塞,甚至还会增加执行时间较短的任务的服务时间。如果线程池中的线程数量远小于在稳定状态下执行时间较长任务的数量,那么到最后可能所有的线程都会运行这些执行时间较长的任务,从而影响整体的响应性。
 有一项技术可以缓解执行时间较长任务造成的影响,即限定任务等待资源的时间,而不要无限制的等待。在平台类库的大多数阻塞方法中,都同时定义了限时版本和无限时版本,例如Thread.joinBlockingQueue.putCountDownLatch.await以及Selector.selector等。如果等待超时,那么可以将任务标识为失败。然后终止任务或者将任务重新放回队列以便随后执行。这样,无论任务的最终结果是否成功,这种办法都能确保任务总能继续执行下去,并将线程释放出来,执行一些能更快完成的任务。如果在线程池中总是充满了被阻塞的任务,那么也有可能表明线程池的规模过小。

设置线程池的大小

 线程池的理想大小取决于被提交任务的类型以及所部署系统的特性。在代码中通常不会固定线程池的大小,而应该通过某种配置机制来提供,或根据Runtime.availableProcessors来动态计算大小。
 幸运的是,要设置线程池的大小也并不困难,只需要避免“过大”和“过小”这两种极端情况。如果线程池过大,那么大量的线程将在相对很少的CPU和内存资源上发生竞争,这不仅会导致更高的内存使用量,而且还有可能耗尽资源。如果线程过小,那么将会导致许多空闲的处理器无法执行工作,从而降低吞吐量。
 要想正确的设置线程池的大小,必须分析计算环境、资源预算和任务的特性。在部署的系统中有多少个CPU?多大的内存?任务是计算密集型、I/O密集型还是二者皆可?它们是否需要像JDBC连接这样的稀缺资源?如果需要执行不同类别的任务,并且它们之间的行为相差很大,那么应该考虑使用多个线程池,从而使每个线程池可以根据自己的工作负载来调整。
 对于计算密集型的任务,在拥有N(cpu)个处理器的系统上,当线程池的大小为N(cpu)+1时,通常能实现最优的利用率。(即使当计算密集型的线程偶尔由于页缺失故障或者其他原因而暂停时,这个“额外”的线程也能确保CPU的时钟周期不会被浪费。)对于包含I/O操作或者其他阻塞操作的任务,由于线程并不会一直执行,因此线程的规模应该更大。要正确的设置线程池的大小,你必须估算出任务的等待时间与计算时间的比值。这种估算不需要很精确,并且可以通过一些分析或者监控工具来获得。你还可以通过另一种方法来调节线程池的大小:在某个基准负载下,分别给不同大小的线程池来运作应用程序,并观察CPU利用率、
 给定下列定义:

N(cpu)=number of CPUs
U(cpu)=targe CPU utilization,0≤U(cpu)≤1
W/C=ratio of wait time to compute time
 要使处理器达到期望的使用率,线程池的最优大小等于
N(threads)=N(cpu)*U(cpu)*(1+W/C)
 可以通过Runtime来获得CPU的数目:
int N_CPUS = Runtime.getRuntimes().availableProcessors();

 当然CPU周期并不是唯一影响线程池大小的资源,还包括内存、文件句柄,套接字句柄和数据库连接池。计算这些资源对线程池的约束条件是更容易的:计算每个任务对该资源的需求量,然后用该资源的可用总量除以每个任务的需求量,所以结果就是线程池大小的上限。
 当任务需要通过某种资源池来管理资源时,例如数据库连接,那么线程池和资源池的大小将会影响。如果每个任务都需要一个数据库连接,那么连接池的大小就限制了线程池的大小。同样,当线程池中的任务是数据库连接的唯一使用者时,那么线程池的大小又将限制连接池的大小。

配置ThreadPollExecutor

ThreadPoolExecutor为一些Executor提供了基本实现,这些Execuor是由Executors中的newCachedThreadPoolnewFixedThreadPoolnewScheduledThreadExecutor等工厂方法返回的,ThreadPoolExecutor是一个灵活的、稳定的线程池,允许进行各种定制。
 如果默认的执行策略不能满足需求,那么可以通过ThreadPoolExecutor的构造函数来实例化一个对象,并根据自己的需求来定制,并且可以参考Executors的源代码来了解默认配置下的执行策略,然后再以这些执行策略为基础进行修改。ThreadPoolExecutor定义了很多构造函数,下面给出了最常见的形式。

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

线程的创建与销毁

 线程池的基本大小、最大大小以及存活时间等因素共同负责线程的创建与销毁。基本大小也就是线程池的目标大小,即在没有任务执行时线程池的大小,,并且只有在工作队列满了的情况下才会创建超出这个数量的线程。线程池的最大大小表示可同时活动的线程数量的上限。如果某个线程的空闲时间超过了存活时间,那么将被标记为可回收的,并且当线程的当前大小超过了基本大小时,这个线程将被终止。
 通过调节线程池的基本大小和存活时间,可以帮助线程池回收空闲线程占用的资源,从而使得这些资源可以用于执行其他工作。(显然,这是折衷:回收空闲线程会产生额外的延迟,因为当需求增加时,必须创建新的线程来满足需求。)
newFixedThreadPool工厂方法将线程池的基本大小设置为参数中指定的值,而且创建的线程池不会超时。newCachedThreadPool工厂方法将线程池的最大大小设置为Integer.MAX_VALUE,而将基本大小设置为零,并将超时设置为1分钟,这种方法创造出来的线程池可以被无限扩展,并且当需求降低时会自动收缩。其他形式的线程可以显示的ThreadPoolExecutor构造函数来构造。

管理队列任务

 在有限的线程池中会限制可并发执行任务数量。(单线程的Executor是一种值得注意的特例:它们能够确保不会有任务并发执行,因为它们通过线程封闭来实现线程安全性)
 如果无限制的创建线程,那么将导致不稳定性,那么则采用固定大小的线程池(而不是每收到一个请求就创建一个新线程)来解决这个问题。然而,这个方案并不完整,在高负载的情况下,应用程序仍可能耗尽资源,只是出现问题的概率较小。如果新请求的到达速率超过了线程池的处理速率,那么新到来的请求将积累起来。在线程池中,这些请求会在一个由Executor管理的Runnable队列中等待,而不会像线程那样去竞争CPU资源。通过一个Runnable和一个链表节点来实现一个等待中的任务,当然比使用线程来表示的开销低很多,但如果客户提交给服务器请求的速率超过了服务器的处理速率,那么仍然会耗尽资源。
 即使请求的平均达到速率很稳定,也仍然会出现请求突增的情况。尽管队列有助于缓解任务的突增问题,但如果任务持续高速地到来,那么最终还是会抑制请求的到达率以避免耗尽内存。甚至在耗尽内存之前,响应性性能也随着任务队列的增长而变得越来越糟糕。
ThreadPoolExecutor允许提供一个BlockingQueue来保存等待执行的任务。基本的任务排队方法有3种:无界队列有界队列同步移交。队列的选择与其他的配置参数有关,例如线程池的大小等。
newFixedThreadPoolnewSingleThreadExecutor在默认的情况下将使用一个无界的LinkedBlockingQueue。如果所有工作者线程都处于忙碌状态,那么任务将在队列中等待。如果任务持续快速的到达,并且超过了线程池处理它们的速度,那么队列将无限制的增加。
 一种更稳妥的资源管理策略是使用有界队列,例如ArrayBlockingQueue、有界的LinkedBlockingQueuePriorityBlockingQueue。有界队列有助于避免资源耗尽的情况发生,但它又带来了新的问题;当队列填满后,新的任务怎么办?(有许多饱和策略可以解决这个问题。)在使用有界的工作队列时队列的大小与线程池的大小必须一起调节。如果线程池较小而队列较大,那么有助于减少内存使用量,降低CPU的使用率,同时还可以减少上下文的切换,但付出的代价可能会限制吞吐量。
 对于非常大的或者无界的线程池,可以通过使用SynchronousQueue来避免任务排队,以及直接将任务从生产者移交到工作者线程。SynchronousQueue不是一个真正的队列,而是一种在线程之间进行移交的机制,要将一个元素放入SynchronousQueue中,必须有另一个线程正在等待接受这个元素。如果没有线程正在等待,并且线程池的当前大小小于最大值,那么ThreadPoolExecutor将创建一个新的线程,否则根据饱和策略,这个任务将被拒绝。使用直接移交将更高效,因为任务会直接移交给执行它的线程,而不是被首先放在队列中,然后有工作者线程从队列中提取该任务。只有当线程池是无界的或者可以拒绝任务时,SynchronousQueue才有实际价值。在newCacheThreadPool工厂方法就使用的是SynchronousQueue
 当使用像LinkedBlockingQueueArrayBlockQueue这样的FIFO(先进先出)队列时,任务的执行顺序与它们的到达顺序相同。如果想进一步控制任务执行顺序,还可以使用PriorityBlockQueue,这个队列将根据优先级来安排任务。任务的优先级是通过自然排序或者Comparator(如果线程实现了Comparable)来定义的。

对于ExecutornewCachedThreadPool工厂方法是一种很好的默认选择,它能提供比固定大小的线程池更好的排队性能。当需要限制当前任务的数量以满足资源管理需要时,那么可以选择固定大小的线程池,就像在接受网络客户请求的服务器应用程序中,如果不进行限制,那么很容易发生过载问题。

 只有当任务相互独立时,为线程池或工作队列设置界限才是合理的。如果任务之间存在依赖性,那么有界的线程池或者队列就可能导致线程“饥饿”死锁的问题。此时应该使用无界的线程池,例如newCachedThreadpool

饱和策略

 当有界队列被填满后,饱和策略开始发挥作用。ThreadPoolExecutor的饱和策略可以通过调用setRejectedExecutionHandler来修改。(如果某个任务被提交到一个已被关闭的Executor时,也会用到饱和策略。)JDK提供了几种不同的RejectedExecutionHandler实现,每种实现都包含不同的饱和策略:AbortPolicyCallerRunsPolicyDiscardPoilcyDiscardOldestPolicy
 “中止(Abort)”策略是默认的饱和策略,该策略将抛出未检查的RejectedExecutionException。调用者可以捕获这个异常,然后根据需求编写自己的处理代码,当提交的任务无法保存到队列中等待执行时,“抛弃(Discard)”策略会悄悄的抛弃该任务。“抛弃最旧的(DiscardOldest)”策略则会抛弃下一个将被执行的任务,然后尝试重新提交新的任务。(如果工作队列是一个优先队列,那么“抛弃最旧的策略”将导致抛弃优先级最高的任务,因此最好不要将“抛弃最旧的”饱和策略和优先级队列放在一起使用。)
 "调用者运行(Caller-Runs)"策略实现了一种调节机制,该策略不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量,它不会在线程池的某个线程中执行新提交的任务,而是在一个调用了execute的线程中执行该任务。当线程池中的所有线程都被占用,并且工作队列被填满后,下一个任务会在调用execute时在主线程执行。由于执行任务需要一定的事件,因此主线程至少在一段时间内不能提交任何任务,从而使得工作者线程有时间来处理正在执行的任务。在这期间,主线程不会调用accept
 当创建Executor时,可以选择饱和策略或者对执行策略进行修改。下面给出了如何创建一个固定大小的线程池,同时使用“调用者运行”饱和策略。

ThreadPoolExecutor executor=new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunPolioy());

 当工作队列被填满后,没有预定义的饱和策略来阻塞execute。然后通过使用Semaphore来限制任务的到达率,就可以实现这个功能。在下面的BoundedExecutor中给出了这种方法。该方法使用了一个无界队列(因为不能限制队列的大小和任务的到达率),并设置信号量的上界设置为线程池的大小加上可排队任务的数量,这是因为信号量需要控制在执行的和等待的任务数量。

public class BoundedExecutor {
    private Executor exec;
    private Semaphore semaphore;

    public BoundedExecutor(Executor exec, int bound) {
        this.exec = exec;
        this.semaphore = new Semaphore(bound);
    }

    public void submit(Runnable command) throws InterruptedException {
        semaphore.acquire();
        try {
            exec.execute(new Runnable() {
                @Override
                public void run() {
                    try {
                        command.run();
                    } finally {
                        semaphore.release();
                    }
                }
            });
        } catch (RejectedExecutionException e) {
            semaphore.release();
        }

    }
}

线程工厂

 每当线程池需要创建一个线程时,都是通过线程工厂方法来完成的。默认的线程工厂方法将创建一个新的,非守护的线程,并且不包含特殊的配置的信息。通过指定一个线程工厂方法,可以制定线程池的配置信息。在ThreadFactory中只定义了一个方法newThread,每当线程池需要创建一个线程池时都会调用这个方法。
 然而,在许多情况下需要使用定制的线程工厂方法。例如,你希望为线程池中的线程指定一个UncaughExceptionHandler,或者实例化一个定制的Thread类用于执行调试信息的记录。你还可以希望修改线程的优先级(不推荐)或者守护状态(不推荐)。或许你只是希望给线程取一个更有意义的名称,用来解释线程的转储信息和错误日志。

public interface ThreadFactory {

    /**
     * Constructs a new {@code Thread}. Implementations may also initialize
     * priority, name, daemon status, {@code ThreadGroup}, etc.
     *
     * @param r a runnable to be executed by new thread instance
     * @return constructed thread, or {@code null} if the request to
     * create a thread is rejected
     */
    Thread newThread(Runnable r);
}

 如果在应用程序中需要利用安全策略来控制对某些特殊代码库的访问权限,那么可以通过Executor中的privilegedThreadFactory工厂来定制自己的线程工厂。通过这种方式创建出来的线程,将与创建privilegedThreadFactory的线程拥有相同的访问权限、AccessControlContextcontextClassLoader。如果不使用privilegedThreadFactory,线程池创建的线程将从在需要新线程时调用executesubmit的客户程序中继承访问权限,从而导致令人困惑的安全性异常。

在调用构造函数后不在定制ThreadPoolExecutor

 在调用完ThreadPoolExecutor的构造函数后,仍然可以通过设置函数(Setter)来修改大多数传递给它的构造函数的参数(例如线程池的基本大小、最大大小、存活时间、线程工厂以及拒绝执行处理器)。如果Executor是通过Executors中的某个(newSingleThreadExecutor除外)工厂方法创建的,那么将可以将结果的类型转换为ThreadPoolExecutor以访问设置器。
 在Executors中包含一个unconfigurableExecutorService工厂方法,该方法对一个现有的ExecutorService进行包装,使其只暴露出ExecutorService的方法,因此不能对它进行配置。newSingleThreadExecutor返回按这种方式封装的ExecutorService,而不是最初的ThreadPoolExecutor。虽然单线程的Executor实际上被实现为一个只包含唯一线程的线程池,但它也同样确保了不会并发的执行任务。如果在代码中增加单线程Executor的线程池大小,那么将破坏它的执行语义。
 可以在自己的Executor中使用这项技术达到以防止执行策略被修改。如果将ExecutorService暴露给不信任的代码。又不希望对其进行修改,就可以通过unconfigurableExecutorService来包装它、

猜你喜欢

转载自blog.csdn.net/lmh_19941113/article/details/85315845