一、线程池相关

1、什么是线程池,如何使用?为什么要使用线程池?

答:线程池就是事先将多个线程对象放到一个容器中,使用的时候就不用new线程而是直接去池中拿线程即可,节 省了开辟子线程的时间,提高了代码执行效率。

2、Java中的线程池共有几种?

Java有四种线程池

  • 第一种:newCachedThreadPool。不固定线程数量:且支持最大为Integer.MAX_VALUE的线程数量:
    public static ExecutorService newCachedThreadPool() {
        // 这个线程池corePoolSize为0,maximumPoolSize为Integer.MAX_VALUE
        // 意思也就是说来一个任务就创建一个woker,回收时间是60s
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                    60L, TimeUnit.SECONDS,
                                    new SynchronousQueue<Runnable>());
    }
    
    • 可缓存线程池:
      • 线程数无限制。
      • 有空闲线程则复用空闲线程,若无空闲线程则新建线程。
      • 一定程序减少频繁创建/销毁线程,减少系统开销。
  • 第二种:newFixedThreadPool。一个固定线程数量的线程池:
    public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
        // corePoolSize跟maximumPoolSize值一样,同时传入一个无界阻塞队列
        // 该线程池的线程会维持在指定线程数,不会进行回收
        return new ThreadPoolExecutor(nThreads, nThreads,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>(),
                                    threadFactory);
    }
    
    • 定长线程池:
      • 可控制线程最大并发数(同时执行的线程数)。
      • 超出的线程会在队列中等待。
  • 第三种:newSingleThreadExecutor。可以理解为线程数量为1的FixedThreadPool:
    public static ExecutorService newSingleThreadExecutor() {
        // 线程池中只有一个线程进行任务执行,其他的都放入阻塞队列
        // 外面包装的FinalizableDelegatedExecutorService类实现了finalize方法,在JVM垃圾回收的时候会关闭线程池
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
    
    • 单线程化的线程池:
      • 有且仅有一个工作线程执行任务。
      • 所有任务按照指定顺序执行,即遵循队列的入队出队规则。
  • 第四种:newScheduledThreadPool。支持定时以指定周期循环执行任务:
    public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
    

注意:前三种线程池是ThreadPoolExecutor不同配置的实例,最后一种是ScheduledThreadPoolExecutor的实例。

从数据结构的角度来看,线程池主要使用了阻塞队列(BlockingQueue)和HashSet集合构成。 从任务提交的流程角度来看,对于使用线程池的外部来说,线程池的机制是这样的:

  1. 如果正在运行的线程数 < coreSize,马上创建核心线程执行该task,不排队等待;
  2. 如果正在运行的线程数 >= coreSize,把该task放入阻塞队列;
  3. 如果队列已满 && 正在运行的线程数 < maximumPoolSize,创建新的非核心线程执行该task;
  4. 如果队列已满 && 正在运行的线程数 >= maximumPoolSize,线程池调用handler的reject方法拒绝本次提交。

理解记忆:1-2-3-4对应(核心线程->阻塞队列->非核心线程->handler拒绝提交)。

线程池的线程复用:

这里就需要深入到源码addWorker():它是创建新线程的关键,也是线程复用的关键入口。最终会执行到runWoker,它取任务有两个方式:

  • firstTask:这是指定的第一个runnable可执行任务,它会在Woker这个工作线程中运行执行任务run。并且置空表示这个任务已经被执行。
  • getTask():这首先是一个死循环过程,工作线程循环直到能够取出Runnable对象或超时返回,这里的取的目标就是任务队列workQueue,对应刚才入队的操作,有入有出。

其实就是任务在并不只执行创建时指定的firstTask第一任务,还会从任务队列的中通过getTask()方法自己主动去取任务执行,而且是有/无时间限定的阻塞等待,保证线程的存活。

信号量

semaphore 可用于进程间同步也可用于同一个进程间的线程同步。

可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。

4、线程池都有哪几种工作队列?

  1. ArrayBlockingQueue: 是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。
  2. LinkedBlockingQueue: 一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()和Executors.newSingleThreadExecutor使用了这个队列。
  3. SynchronousQueue: 一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。
  4. PriorityBlockingQueue: 一个具有优先级的无限阻塞队列。

5、怎么理解无界队列和有界队列?

有界队列
  1. 初始的 poolSize < corePoolSize,提交的runnable任务,会直接做为new一个Thread的参数,立马执行 。
  2. 当提交的任务数超过了corePoolSize,会将当前的runable提交到一个block queue中。
  3. 有界队列满了之后,如果poolSize < maximumPoolsize时,会尝试new 一个Thread的进行救急处理,立马执行对应的runnable任务。
  4. 如果3中也无法处理了,就会走到第四步执行reject操作。
无界队列

与有界队列相比,除非系统资源耗尽,否则无界的任务队列不存在任务入队失败的情况。当有新的任务到来,系统的线程数小于corePoolSize时,则新建线程执行任务。当达到corePoolSize后,就不会继续增加,若后续仍有新的任务加入,而没有空闲的线程资源,则任务直接进入队列等待。若任务创建和处理的速度差异很大,无界队列会保持快速增长,直到耗尽系统内存。 当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略。

Java提供的线程安全的Queue可以分为阻塞队列和非阻塞队列,其中阻塞队列的典型例子是BlockingQueue,非阻塞队列的典型例子是ConcurrentLinkedQueue.

对于BlockingQueue,想要实现阻塞功能,需要调用put(e) take() 方法。而ConcurrentLinkedQueue是基于链接节点的、无界的、线程安全的非阻塞队列。