zoukankan      html  css  js  c++  java
  • 26

    一、线程池的基础
    1、线程池的相关属性:
    corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。

    workQueue任务队列):用于保存等待执行的任务的阻塞队列。可以选择以下几个阻塞队列。

    ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
    LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列
    SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
    PriorityBlockingQueue:一个具有优先级的无限阻塞队列。
    maximumPoolSize(线程池最大大小):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。

    ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程做些更有意义的事情,比如设置daemon和优先级等等

    RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。以下是JDK1.5提供的四种策略。

    AbortPolicy:直接抛出异常。
    CallerRunsPolicy:只用调用者所在线程来运行任务。
    DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。
    DiscardPolicy:不处理,丢弃掉。
    也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务。

    keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
    TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。
    2、线程池的执行流程
    1 如果运行的线程少于corePoolSize,则会添加新的线程,而不进行排队。
    2 如果运行的线程等于或多于 corePoolSize,则 Executor 始终首选将请求加入队列,而不添加新的线程。
    3 如果无法将请求加入队列(队列已满),则创建新的线程,除非创建此线程超出 maximumPoolSize,如果超过,在这种情况下,新的任务将被拒绝。
    3、线程池排队有三种通用策略
    同步移交。队列的默认选项是同步移交,它将任务直接提交给线程而不保持它们。在此,如果不存在可用于立即运行任务的线程,会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。同步移交通常要求无界的maximumPoolSizes以避免拒绝新提交的任务。
    但是,同步移交不适合管理资源的分配,除非特殊情况不推荐使用

    无界队列。使用的是LinkedBlockingQueue类实现,不需要事先制定大小,也是按照“先进先出”算法处理任务。无界队列很好理解,就是和有界队列相反,使用无界队列的线程池,当有新任务提交时,如果线程池里有空闲线程,就分配线程立刻执行任务,否则就把任务放到无界任务队列中等待,如果线程池中一直没有空闲线程,但是新的任务又一直不停的提交上来,那么这些任务全部会被挂到等待队列中,一直到内存全部消耗完。
    有界队列。当使用有限的 maximumPoolSizes 时,有界队列(如 ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低CPU使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。使用小型队列通常要求较大的池大小,CPU 使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。
    我们最后可以这样理解:

    使用无界队列可能会耗尽系统资源。
    使用有界队列可能不能很好的满足性能,需要调节线程数和队列大小。
    线程数也有开销,所以需要根据不同应用进行调节。
    二、设置线程池的大小
    线程池的大小一直是大家很关心的问题,理想的大小取决于被提交任务的类型以及所部署的系统,代码中通常不会固定线程池的大小,而通过某种配置,或者Runtime.getRuntime().availableProcessors() 来动态计算。

    Runtime.getRuntime().availableProcessors()这个代码大家可能不算太熟悉,这个方法可以获取CPU的数目。

    如果是CPU密集型应用,则线程池大小设置为N+1(或者是N),线程的应用场景:主要是复杂算法
    如果是IO密集型应用,则线程池大小设置为2N+1(或者是2N),线程的应用场景:主要是:数据库数据的交互,文件上传下载,网络数据传输等等。
    综合起来最佳线程数目 = ((线程等待时间+线程CPU时间)/线程CPU时间 +1)* CPU数目。
    至于+1的原因,则是当线程偶尔由于缺失故障或者其他原因而暂停时,这个额外的线程也能确保CPU的时钟周期不会被浪费(剩余价值压榨的满满的)。
    当然,CPU并不是唯一影响线程池大小的资源,还应该考虑内存、文件句柄、套接字句柄、数据库连接等原因。
    举个例子,比如平均每个线程CPU运行时间为0.5s,而线程等待时间(非CPU运行时间,比如IO)为1.5s,CPU核心数为12,那么根据上面这个公式估算得到: ((0.5+1.5)/0.5+1)12=60

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

    三、避免线程池的饥饿死锁
    1、线程的饥饿死锁
    线程池中,如果任务依赖于其他任务,那么就有可能产生死锁。在单线程的Executor中,如果一个任务将另一个任务提交到同一个Executor,并且等待这个被提交的任务的结果,那么就通常会产生死锁,这种情况被称为线程的饥饿死锁。

    对于线程池来说,只要池任务开始了无限期阻塞,例如某个任务的目的是等待一些资源或条件,但是只有另一个池任务的执行才能使那些条件成立。除非能保证线程池足够大,否则会发生线程饥饿死锁。

    下文清晰地展示了线程饥饿死锁的示例,队列为阻塞队列,因为线程池为是单线程的,当队列为空时,getHeader 将会一直阻塞等待 putHeader 执行。这就是任务之间相互依赖的饥饿死锁。

    public class ThreadDeadlock {
    //创建一个队列、假设是存放头文件的地方
    private static BlockingQueue root = new ArrayBlockingQueue(10);
    public static void main(String[] args) {
    //创建一个固定线程的线程池
    ExecutorService service = Executors.newSingleThreadExecutor();
    service.submit(new getHeader());
    service.submit(new putHeader(1));
    service.shutdown();
    }
    static class putHeader implements Callable {
    private int val;
    public putHeader(int value) {
    val = value;
    }
    @Override
    public Object call() throws Exception {
    System.out.println(“放置头文件”);
    //往阻塞队列增加元素
    root.put(1);
    return “头文件”;
    }
    }
    static class getHeader implements Callable {
    @Override
    public Object call() throws Exception {
    System.out.println(“获取头文件”);
    //取出阻塞队列的值,如果没有则会阻塞
    int value = (int) root.take();
    return “头文件”;
    }
    }
    }
    2、运行时间较长的任务
    如果任务阻塞的时间过长,那么即使不出现死锁,线程池的响应性也会变得更糟。执行时间较长的任务不会造成线程池的堵塞,甚至还会增加执行时间较短任务的服务时间。如果线程池中的线程数量远小于在稳定状态下执行的任务的数量,那么到最后可能所有的线程都会运行这些执行时间较长的任务,从而影响整体的响应性。

    可以通过限定任务等待资源的时间,不要去无限制地等待。在平台类库的大多数可阻塞方法中,都同时定义了限时版本和无限时版本,列如Thread.join、BlockingQueue.out、CountDownLatch.await以及Selector.select等。如果等待超时,那么可以把任务标识为失败,然后中止任务或者将任务重新放回队列。这样,无论任务的最终结果是否是成功,这种办法都能保证任务可以顺利执行而不会被阻塞住,并将线程释放出来执行一些能更快完成的任务。

    当然了,如果线程池中总是充满了被阻塞的任务,也说明线程池设计的规模小了。

  • 相关阅读:
    FZU 2150 Fire Game
    POJ 3414 Pots
    POJ 3087 Shuffle'm Up
    POJ 3126 Prime Path
    POJ 1426 Find The Multiple
    POJ 3278 Catch That Cow
    字符数组
    HDU 1238 Substing
    欧几里德和扩展欧几里德详解 以及例题CodeForces 7C
    Codeforces 591B Rebranding
  • 原文地址:https://www.cnblogs.com/gd11/p/14215750.html
Copyright © 2011-2022 走看看