【多线程】线程池

【多线程】线程池

    • 线程池的优势
    • 创建线程池
      • 构造方法
      • 便捷创建方法
    • 提交任务到线程池的方式
    • 使用线程池的注意点
    • 面试题
        • 面试问题1:Java的线程池说一下,各个参数的作用,如何进行的?
        • 面试问题2:使用线程池如何处理异常?
        • 面试问题3:线程池都有哪几种工作队列?
        • 面试问题4:说说几种常见的线程池及使用场景?
        • 面试问题5:线程池有哪些状态?
        • 面试问题6:讲讲newScheduledThreadPool工作机制

线程池的优势

  1. 管理线程
    复用线程、控制最大并发数。对线程进行统一分配、调优和监控。
  2. 降低资源消耗
    通过重复利用已创建的线程,降低线程创建、销毁的资源开销。
  3. 提高响应速度
    任务不需要创建线程即可使用现有线程立刻执行。
  4. 实现任务线程队列的 缓存策略拒绝机制
  5. 控制线程执行时间
    如定时任务、周期执行
  6. 隔离线程环境
    如果一个JVM中有两个服务,服务A对性能要求高,服务B相对较低,为了避免B服务消耗资源过多影响A服务效率,可以将两个服务的线程在不同的线程池,控制资源分配。

创建线程池

构造方法

ThreadPoolExecutor的构造方法如下:

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.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
  • corePoolSize: 线程池核心线程数最大值。
  • maximumPoolSize: 线程池最大线程数大小。corePoolSize和maximumPoolSize设置不当会影响效率,甚至耗尽线程资源。
  • keepAliveTime: 线程池中非核心线程空闲的存活时间大小
  • unit: 线程空闲存活时间单位
  • workQueue: 存放任务的阻塞队列。workQueue设置不当会导致OOM。
  • threadFactory: 用于设置创建线程的工厂,可以给创建的线程设置有意义的名字,可方便排查问题。
  • handler: 线城池的饱和策略事件,主要有四种类型。handler设置不当会导致提交任务时抛出异常。
线程池中线程工作顺序:
如果正在执行总线程数小于corePoolSize,Executor 会优先创建新线程到线程池;
如果正在执行总线程数大于corePoolSize,Executor 通常会将线程请求加入workQueue;
如果线程请求不能被加入workQueue,Executor 会创建新线程,除非当前正在运行的线程数大于maximumPoolSize,这种情况下Executor会根据拒绝策略拒绝任务。

		corePoolSize -> 任务队列 -> maximumPoolSize -> 拒绝策略

【多线程】线程池_第1张图片

便捷创建方法

Executors提供了创建线程池的便捷方法,如:Executors.newFixedThreadPool(int nThreads),但此方法隐藏了线程池的复杂性,会埋下隐患(定义一个Integer.MAX_VALUE大小的LinkedBlockingQueue,导致OOM; 无上限地创建线程,导致线程耗尽)

创建线程池的便捷方法 功能 应用场景 注意事项
Executors.newFixedThreadPool(10) 创建固定大小的线程池,可控制最大并发数 适用于处理CPU密集型的任务,确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务。 线程池大小最好依赖系统资源定义,如Runtime.getRuntime().availableProcessors()
Executors.newSingleThreadExecutor() 创建一个线程的线程池(corePoolSize和maximumPoolSize都为1) 适用于串行执行任务的场景,一个任务一个任务地执行。
Executors.newCachedThreadPool() 创建可缓存线程池(corePoolSize=0,keepAliveTime=60s说明线程资源释放后可以缓存一分钟) 用于并发执行大量短期的小任务 线程数无上限,如果任务处理速度 < 任务提交速度,就会不断创建新线程导致资源耗尽。所以该线程池适合执行时间较短的任务
Executors.newScheduledThreadPool(1) 创建一个定制执行线程的线程池 周期性执行任务的场景,需要限制线程数量的场景

提交任务到线程池的方式

提交方式 释放关心返回结果
Future submit(Callable task)
void execute(Runnable command)
Future submit(Runnable command) 否。会返回Future,不过get()得到的永远是null;
Runnable和Callable的区别
1、方法签名不同。void Runnable.run(), V callable.call() throw Exception
2、Runnable 无返回值,Callable有返回值
3、Runnable不可抛出异常,Callable可以抛出异常

使用线程池的注意点

  1. 避免使用无界队列
    Executors提供的快捷方式默认使用的workQueue都是无界队列或者容量很大的队列,为避免OOM和线程资源耗尽,我们应该手动使用ThreadPoolExecutor的构造方法定义线程池。
ExecutorService executorService = new ThreadPoolExecutor(2, 2,
                0, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(512), // 使用有界队列,避免OOM
                new ThreadPoolExecutor.DiscardPolicy());
  1. 明确线程池任务拒绝策略
拒绝策略 拒绝行为
AbortPolicy(默认) 抛出RejectedExecutionException异常
CallerRunsPolicy 直接由任务提交线程执行该任务
DiscardOldestPolicy 丢弃workQueue中最老的任务,尝试将新任务加入到workQueue中
DiscardPolicy 什么都不做,直接丢弃新任务

面试题

面试问题1:Java的线程池说一下,各个参数的作用,如何进行的?

答:如上文所述。

面试问题2:使用线程池如何处理异常?

答:1、 线程内try-catch捕获处理;
2、submit Callable线程,通过Future.get()获取线程异常并处理。
3、重写ThreadPoolExecutor的afterExecute()方法,处理传递的异常引用。

class ExtendedExecutor extends ThreadPoolExecutor {
    // 这可是jdk文档里面给的例子。。
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        if (t == null && r instanceof Future<?>) {
            try {
                Object result = ((Future<?>) r).get();
            } catch (CancellationException ce) {
                t = ce;
            } catch (ExecutionException ee) {
                t = ee.getCause();
            } catch (InterruptedException ie) {
                Thread.currentThread().interrupt(); // ignore/reset
            }
        }
        if (t != null)
            System.out.println(t);
    }
}}

4、为工作线程设置UncaughtExceptionHandler,在uncaughtException中处理异常。

ExecutorService threadPool = Executors.newFixedThreadPool(1, r -> {
            Thread t = new Thread(r);
            t.setUncaughtExceptionHandler(
                    (t1, e) -> {
                        System.out.println(t1.getName() + "线程抛出的异常"+e);
                    });
            return t;
           });
        threadPool.execute(()->{
            Object object = null;
            System.out.print("result## " + object.toString());
        });

面试问题3:线程池都有哪几种工作队列?

答:

  1. ArrayBlockingQueue
    有界队列。用数组实现的阻塞队列,按FIFO排序任务。
  2. LinkedBlockingQueue
    可设置容量队列。基于链表实现的阻塞队列,按FIFO排序任务,可选择性设置容量大小,默认为最大长度Integer.MAX_VALUE。newFixedThreadPool、newSingleThreadExecutor等线程池使用了它。
  3. DelayedWorkQueue
    延时队列。定时周期性地执行任务根据指定的执行时间顺序执行,否自按FIFO顺序执行。
  4. PriorityBlockingQueue
    优先级队列。是具有优先级的无界阻塞队列。
  5. SynchronousQueue
    同步队列。一个不存储元素的阻塞队列,每一个插入操作必须等到另一个线程调用移除操作,否则插入一直处于阻塞状态。newCachedThreadPool使用了该队列。

面试问题4:说说几种常见的线程池及使用场景?

答:见上文。

面试问题5:线程池有哪些状态?

答:
【多线程】线程池_第2张图片

RUNNING

  • 该状态的线程池会接收新任务,并处理阻塞队列中的任务;
  • 调用线程池的shutdown()方法,可以切换到SHUTDOWN状态;
  • 调用线程池的shutdownNow()方法,可以切换到STOP状态;

SHUTDOWN

  • 该状态的线程池不会接收新任务,但会处理阻塞队列中的任务;
  • 队列为空,并且线程池中执行的任务也为空,进入TIDYING状态;

STOP

  • 该状态的线程不会接收新任务,也不会处理阻塞队列中的任务,而且会中断正在运行的任务;
  • 线程池中执行的任务为空,进入TIDYING状态;

TIDYING

  • 该状态表明所有的任务已经运行终止,记录的任务数量为0。
  • terminated()执行完毕,进入TERMINATED状态

TERMINATED

  • 该状态表示线程池彻底终止

面试问题6:讲讲newScheduledThreadPool工作机制

答:

  • 添加一个任务
  • 线程池中的线程从 DelayQueue 中取任务
  • 线程从 DelayQueue 中获取 time 大于等于当前时间的task
  • 执行完后修改这个 task 的 time 为下次被执行的时间
  • 这个 task 放回DelayQueue队列中
    /**
    创建一个给定初始延迟的间隔性的任务,之后的下次执行时间是上一次任务从执行到结束所需要的时间+* 给定的间隔时间
    */
    ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
        scheduledExecutorService.scheduleWithFixedDelay(()->{
            System.out.println("current Time" + System.currentTimeMillis());
            System.out.println(Thread.currentThread().getName()+"正在执行");
        }, 1, 3, TimeUnit.SECONDS);
   /**
    创建一个给定初始延迟的间隔性的任务,之后的每次任务执行时间为 初始延迟 + N * delay(间隔) 
    */
    ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
            scheduledExecutorService.scheduleAtFixedRate(()->{
            System.out.println("current Time" + System.currentTimeMillis());
            System.out.println(Thread.currentThread().getName()+"正在执行");
        }, 1, 3, TimeUnit.SECONDS);;

你可能感兴趣的:(多线程)