Java 线程池线程池

线程池

2016-11-11  本文已影响260人  TTTqiu

作者:肥肥鱼链接:https://www.zhihu.com/question/30804052/answer/49562693
来源:知乎著作权归作者所有


简单使用

Java里面线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。
比较重要的几个类:

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

// 一个没有限制最大线程数的线程池
ExecutorService mCacheThreadExecutor = Executors.newCachedThreadPool();
// 限制线程池大小为count的线程池
ExecutorService mFixedThreadExecutor = Executors.newFixedThreadPool(count);
// 一个可以按指定时间可周期性的执行的线程池
ExecutorService mScheduledThreadExecutor = Executors.newScheduledThreadPool(count);
// 每次只执行一个线程任务的线程池 }
ExecutorService mSingleThreadExecutor = Executors.newSingleThreadExecutor();
        executorService.execute(new Runnable() { // or .submit 区别见下文
            @Override
            public void run() {
               // something
            }
        });
        ......
        executorService.shutdown(); // or .shutdownNow() 区别见下文

四种线程池各自的特点

  1. newCachedThreadPool() 缓存型池子,先查看池中有没有以前建立的线程,如果有,就reuse.如果没有,就建一个新的线程加入池中。能reuse的线程,必须是timeout IDLE内的池中线程,缺省timeout是60s,超过这个IDLE时长,线程实例将被终止及移出池。缓存型池子通常用于执行一些生存期很短的异步型任务 。
  2. newFixedThreadPool() fixedThreadPool与cacheThreadPool差不多,也是能reuse就用,但不能随时建新的线程 其独特之处:任意时间点,最多只能有固定数目的活动线程存在,此时如果有新的线程要建立,只能放在另外的队列中等待,直到当前的线程中某个线程终止直接被移出池子。和cacheThreadPool不同:fixedThreadPool池线程数固定,但是0秒IDLE(无IDLE)。这也就意味着创建的线程会一直存在。所以fixedThreadPool多数针对一些很稳定很固定的正规并发线程,多用于服务器。
  3. newScheduledThreadPool() 调度型线程池。这个池子里的线程可以按schedule依次delay执行,或周期执行 。0秒IDLE(无IDLE)。
  4. newSingleThreadExecutor() 单例线程,任意时间池中只能有一个线程 。用的是和cache池和fixed池相同的底层池,但线程数目是1-1,0秒IDLE(无IDLE)。

原理


一般情况下,如果使用子线程去执行一些任务,那么使用 new Thread 的方式会很方便的创建一个线程,如果涉及到主线程和子线程的通信,我们将使用 Handler(一般需要刷新 UI 的适合用到)。

如果我们创建大量的(特别是在短时间内,持续的创建生命周期较长的线程)野生线程,往往会出现如下两方面的问题:

  1. 每个线程的创建与销毁(特别是创建)的资源开销是非常大的;
  2. 大量的子线程会分享主线程的系统资源,从而会使主线程因资源受限而导致应用性能降低。

各位开发一线的前辈们为了解决这个问题,引入了线程池(ThreadPool)的概念,也就是把这些野生的线程圈养起来,统一的管理他们。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

那么线程池是如何使用的呢?

我们可以通过ThreadPoolExecutor来创建一个线程池。

new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, milliseconds,runnableTaskQueue, handler);

创建一个线程池需要输入几个参数:

  1. corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的 prestartAllCoreThreads 方法,线程池会提前创建并启动所有基本线程。
  2. runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。 可以选择以下几个阻塞队列。
  1. maximumPoolSize(线程池最大大小):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。
  2. ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。
  3. RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是 AbortPolicy,表示无法处理新任务时抛出异常。以下是JDK1.5提供的四种策略。
  1. keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。
  2. TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。

如何向线程池提交线程任务呢?

  1. 我们可以使用线程池的 execute 提交的任务,但是 **execute **方法没有返回值,所以无法判断任务是否被线程池执行成功:
threadsPool.execute(new Runnable() { 
        @Override 
        public void run() { 
            // TODO Auto-generated method stub 
        } 
    });
  1. 我们也可以使用 submit 方法来提交任务,它会返回一个 future,那么我们可以通过这个 *future 来判断任务是否执行成功,通过 futureget 方法来获取返回值,get ***方法会阻塞住直到任务完成,而使用 ***get(long timeout, TimeUnit unit) ***方法则会阻塞一段时间后立即返回,这时有可能任务没有执行完:
        Future<Object> future = executor.submit(harReturnValuetask);
        try {
            Object s = future.get();
        } catch (InterruptedException e) { 
            // 处理中断异常
        } catch (ExecutionException e) { 
            // 处理无法执行任务异常
        } finally { 
            // 关闭线程池 
            executor.shutdown();
        }

线程池是如何关闭的呢?

ThreadPoolExecutor 提供了两个方法,用于线程池的关闭,分别是 shutdown()shutdownNow()**,其中:

线程池的原理?

线程池中比较重要的规则:

  1. 如果运行的线程少于 corePoolSize,则创建新线程来处理请求,即使其他辅助线程是空闲的;
  2. 如果设置的 corePoolSize 和 **maximumPoolSize **相同,则创建的线程池是大小固定的,如果运行的线程数与 **corePoolSize **相同,当有新请求过来时,若 **workQueue **未满,则将请求放入 **workQueue **中,等待有空闲的线程去从 **workQueue **中取任务并处理
  3. 如果运行的线程多于 corePoolSize 而少于 maximumPoolSize,则仅当队列满时才创建新线程去处理请求;
  4. 如果运行的线程多于 corePoolSize 并且等于 maximumPoolSize,若队列已经满了,则通过**RejectedExecutionHandler **所指定的策略来处理新请求;
  5. 如果将 maximumPoolSize 设置为基本的无界值(如** Integer.MAX_VALUE**),则允许池适应任意数量的并发任务
  1. 直接提交。工作队列的默认选项是 SynchronousQueue,它将任务直接提交给线程而不保持它们。在此,如果不存在可用于立即运行任务的线程,则试图把任务加入队列将失败,因此会构造一个新的线程。此策略可以避免在处理可能具有内部依赖性的请求集时出现锁。直接提交通常要求无界 maximumPoolSizes 以避免拒绝新提交的任务。当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性;
  2. 无界队列。使用无界队列(例如,不具有预定义容量的 LinkedBlockingQueue)将导致在所有 corePoolSize 线程都忙时新任务在队列中等待。这样,创建的线程就不会超过 corePoolSize(因此,maximumPoolSize 的值也就无效了)。当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web 页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性;
  3. 有界队列。当使用有限的 maximumPoolSizes 时,有界队列(如 ArrayBlockingQueue)有助于防止资源耗尽,但是可能较难调整和控制。队列大小和最大池大小可能需要相互折衷:使用大型队列和小型池可以最大限度地降低 CPU 使用率、操作系统资源和上下文切换开销,但是可能导致人工降低吞吐量。如果任务频繁阻塞(例如,如果它们是 I/O 边界),则系统可能为超过您许可的更多线程安排时间。使用小型队列通常要求较大的池大小,CPU 使用率较高,但是可能遇到不可接受的调度开销,这样也会降低吞吐量。

接下来我们看一下 ThreadPoolExecutor 中最重要的 execute 方法:

    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        //如果线程数小于基本线程数,则创建线程并执行当前任务
        if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
            // 如线程数大于等于基本线程数或线程创建失败,则将当前任务放到工作队列中。
            if (runState == RUNNING && workQueue.offer(command)) {
                if (runState != RUNNING || poolSize == 0)
                    ensureQueuedTaskHandled(command);
            }
            // 如果线程池不处于运行中或任务无法放入队列,并且当前线程数量小于最大允许的线程数量,则创建一个线程执行任务。
            else if (!addIfUnderMaximumPoolSize(command))
                // 抛出RejectedExecutionException异常
                reject(command); // is shutdown or saturated }
        }
    }

线程池容量的动态调整?

ThreadPoolExecutor 提供了动态调整线程池容量大小的方法:setCorePoolSize() setMaximumPoolSize():**

当上述参数从小变大时,**ThreadPoolExecutor **进行线程赋值,还可能立即创建新的线程来执行任务。

线程池的监控?

通过线程池提供的参数进行监控。线程池里有一些属性在监控线程池的时候可以使用

通过扩展线程池进行监控。通过继承线程池并重写线程池的 beforeExecute,**afterExecute **和 **terminated **方法,我们可以在任务执行前,执行后和线程池关闭前干一些事情。如监控任务的平均执行时间,最大执行时间和最小执行时间等。

使用线程池的风险?

虽然线程池是构建多线程应用程序的强大机制,但使用它并不是没有风险的。用线程池构建的应用程序容易遭受任何其它多线程应用程序容易遭受的所有并发风险,诸如同步错误和死锁,它还容易遭受特定于线程池的少数其它风险,诸如与池有关的死锁、资源不足和线程泄漏。

上一篇 下一篇

猜你喜欢

热点阅读