Java工程师面试1000题222-Java并发编程及线程池

222、Java并发编程及线程池

一、ThreadPoolExecutor

Java并发编程及线程池是BAT等大公司面试必问知识点,因此必须好好掌握。

提到并发编程我们首先会想到线程池,为什么要使用线程池?主要有三点:

第一、使用线程池可以减少在创建和销毁线程上所花费的时间以及系统资源的开销,如果不使用线程池,可能会造成系统频繁创建和销毁大量线程;

第二、使用线程池可以提高响应速度;

第三、使用线程池可以提高线程的可管理性。

介绍线程池需要首先介绍一下ThreadPoolExecutor;java.uitl.concurrent.ThreadPoolExecutor类是线程池中最核心的一个类。

该类有四个构造方法:

    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }
   public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             threadFactory, defaultHandler);
    }
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              RejectedExecutionHandler handler) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), handler);
    }
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

通过观察上面四个构造方法可以发现,该类主要用到下面7个参数:

  1. corePoolSize:核心线程数。默认情况下,在创建了线程池后,线程池中的线程数为0,除非调用了prestartAllCoreThread()或者prestartCoreThread()方法来预创建线程,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把新的任务先放到缓存队列当中去;
  2. maximumPoolSize :最大线程数 ,当新任务不断来临,并且阻塞队列装不下的后 ,会继续创建新的线程来执行任务,直到线程数达到maximumPoolSize 。
  3. keepAliveTime :超时时间,线程池中当前的空闲线程服务完某任务后的存活时间。如果时间足够长,那么可能会服务其它任务。keepAliveTime表示线程没有任务执行时最多保持多久时间会被销毁。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会被销毁,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0。
  4. unit :时间单位
  5. workQueue :阻塞队列 线程数大于核心线程后放到队列中
  6. threadFactory :线程池工厂
  7. handler :拒绝策略 阻塞队列满了,也达到了最大线程数 执行拒绝策略。

再来回顾一下线程池运行流程:

  1. 当线程池小于corePoolSize时,新任务将创建一个新的线程,即使此时线程池种存在空闲线程。
  2. 当线程池达到corePoolSize时,新提交的任务将被放入workQueue中,等待线程池任务调度执行。
  3. 当workQueue已满,且maximumPoolSize>corePoolSize时,新任务会创建新线程执行任务。
  4. 当提交任务数超过maximumPoolSize时,新提交任务由RejectedExecutionHandler处理。
  5. 当线程池中超过corePoolSize时,空闲时间达到keepAliveTime时,关闭空闲线程。
  6. 当设置了allowCoreThreadTimeOut(true)时,线程池中corePoolSize线程空闲时间达到keepAliveTime也将关闭。

阻塞队列

  • ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;
  • LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;
  • synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务

拒绝策略

  1. AbortPolicy :默认 直接抛弃 并抛异常;
  2. DiscardPolicy:直接抛弃 不抛异常;
  3. CallerRunsPolicy: 在主线程中执行;用于被拒绝任务的处理程序,它直接在 execute 方法的调用线程中运行被拒绝的任务;如果执行程序已关闭,则会丢弃该任务。
  4. DiscardOldestPolicy: 把注册队列中最老的抛弃掉 执行当前的;
  5. 自定义的策略: 实现RejectedExecutionHandler即可;

线程池的状态:首先,线程池是一个有状态的对象。状态有以下几种:

· RUNNING: 运行中。此时线程池能接受任务,并且会处理队列中的任务;

· SHUTDOWN: 关闭中。此时,线程池不接受新任务,但是会处理队列中的任务;

· STOP: 停止。此时线程池不接受新任务,也不会处理队列中的任务,还会中断worker线程。

· TIDYING: 清理中。所有任务都已终止且线程数等于0,开始调用terminated()。

· TERMINATED: 终止。terminated()执行结束。

二、线程池种类

1、newCachedThreadPool:用newCachedThreadPool()方法创建该线程池对象,创建之初里面一个线程都没有,当execute方法或submit方法向线程池提交任务时,会自动新建线程;如果线程池中有空余线程,则不会新建;这种线程池一般最多情况可以容纳几万个线程,里面的线程空余60s会被回收。适用场景:执行很多短期异步的小程序。

源码,有两个重载。

    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }
    public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>(),
                                      threadFactory);
    }

2newFixedThreadPool:固定线程数的池子,每个线程的存活时间是无限的,当池子满了就不再添加线程;若池中线程均在繁忙状态,新任务会进入阻塞队列中(无界的阻塞队列)。适用场景:执行长期的任务,性能较好。

    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
    public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>(),
                                      threadFactory);
    }

3、newSingleThreadExecutor:只有一个线程的线程池,且线程的存活时间是无限的;当线程繁忙时,对于新任务会进入阻塞队列中(无界的阻塞队列)。适用:一个任务一个任务执行的场景。

    public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
    public static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory) {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory));
    }

4、NewScheduledThreadPool:创建一个固定大小的线程池,池内的线程存活时间无限,线程池支持定时及周期性的任务执行。如果所有线程均处于繁忙状态,对于新任务会进入DelayedWorkQueue队列。适用场景:周期性执行任务的场景。

    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
    public static ScheduledExecutorService newScheduledThreadPool(
            int corePoolSize, ThreadFactory threadFactory) {
        return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
    }

三、如何合理的配置线程池

要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来进行分析:

  1. 任务的性质:CPU密集型任务,IO密集型任务和混合型任务。
  2. 任务的优先级:高,中和低。
  3. 任务的执行时间:长,中和短。
  4. 任务的依赖性:是否依赖其他系统资源,如数据库连接。
  • 任务性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务配置尽可能少的线程数量,如配置Ncpu+1个线程的线程池。IO密集型任务则由于需要等待IO操作,线程并不是一直在执行任务,则配置尽可能多的线程,如2*Ncpu。混合型的任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。我们可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。
  • 优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。
  • 执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。
  • 依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,如果等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU。
  • 建议使用有界队列,有界队列能增加系统的稳定性和预警能力,可以根据需要设大一点,比如几千。

四、Executor vs ExecutorService vs Executors的区别:

正如上面所说,这三者均是 Executor 框架中的一部分。Java 开发者很有必要学习和理解他们,以便更高效的使用 Java 提供的不同类型的线程池。总结一下这三者间的区别,以便大家更好的理解:

  • Executor 和 ExecutorService 这两个接口主要的区别是:ExecutorService 接口继承了 Executor 接口,是 Executor 的子接口
  • Executor 和 ExecutorService 第二个区别是:Executor 接口定义了 execute()方法用来接收一个Runnable接口的对象,而 ExecutorService 接口中的 submit()方法可以接受Runnable和Callable接口的对象。
  • Executor 和 ExecutorService 接口第三个区别是 Executor 中的 execute() 方法不返回任何结果,而 ExecutorService 中的 submit()方法可以通过一个 Future 对象返回运算结果。
  • Executor 和 ExecutorService 接口第四个区别是除了允许客户端提交一个任务,ExecutorService 还提供用来控制线程池的方法。比如:调用 shutDown() 方法终止线程池。可以通过 《Java Concurrency in Practice》 一书了解更多关于关闭线程池和如何处理 pending 的任务的知识。
  • Executors 类提供工厂方法用来创建不同类型的线程池。比如: newSingleThreadExecutor() 创建一个只有一个线程的线程池,newFixedThreadPool(int numOfThreads)来创建固定线程数的线程池,newCachedThreadPool()可以根据需要创建新的线程,但如果已有线程是空闲的会重用已有线程。

猜你喜欢

转载自blog.csdn.net/qq_21583077/article/details/89188242