Java-并发面试题

线程池相关 (⭐⭐⭐)

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

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

Java 中的线程池共有几种?

Java 有四种线程池:

第一种:newCachedThreadPool

不固定线程数量,且支持最大为 Integer.MAX_VALUE 的线程数量:

1
2
3
4
5
6
7
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>());
}

可缓存线程池:

  1. 线程数无限制。
  2. 有空闲线程则复用空闲线程,若无空闲线程则新建线程。
  3. 一定程度减少频繁创建/销毁线程,减少系统开销。

第二种:newFixedThreadPool

一个固定线程数量的线程池:

1
2
3
4
5
6
7
8
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory){
//corePoolSize跟 maxnumPoolSize 值一样,同时传入一个无界阻塞队列
//该线程池的线程会维持在指定线程数,不会进行回收
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}

定长线程池:

  1. 可控制线程最大并发数(同时执行的线程数)
  2. 超出的线程会在队列中等待

第三种:newSingleThreadExecutor

可以理解为线程数量为 1 的 FixedThreadPool:

1
2
3
4
5
6
public static ExecutorService newSingleThreadExecutor() {
// 线程池中只有一个线程进行任务执行,其他的都放入阻塞队列
// 外面包装的 FinalizableDelegatedExecutorService 类实现了 finalize 方法,在 JVM 垃圾回收的时候会关闭线程池
return new FinalizableDelegatedExecutorService (
new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>())); }

单线程化的线程池 :

  1. 有且仅有一个工作线程执行任务
  2. 所有任务按照指定顺序执行,即遵循队列的入队出队规则

第四种:newScheduledThreadPool

支持定时以指定周期循环执行任务:

1
2
3
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,线程池调用handlerreject方法拒绝本次提交
    理解记忆:1-2-3-4对应(核心线程->阻塞队列->非核心线程->handler拒绝提交)

线程池的线程复用:

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

  • firstTask:这是指定的第一个 runnable 可执行任务,它会在 Woker 这个工作线程中运行执行任务 run。并且置空表示这个任务已经被执行。
  • getTask():这首先是一个死循环过程,工作线程循环直到能够取出 Runnable 对象或超时返回,这里的取的目标就是任务队列 workQueue,对应刚才入队的操作,有入有出。
    其实就是任务在并不只执行创建时指定的 firstTask 第一任务,还会从任务队列的中通过 getTask()方法自己主动去取任务执行,而且是有/无时间限定的阻塞等待,保证线程的存活。

信号量

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

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

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

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

有界队列

  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提供的常用方法

可能报异常 返回布尔值 可能阻塞 设定等待时间
入队 add(e) offer(e) put(e) offer(e, timeout, unit)
出队 remove() poll() take() poll(timeout, unit)
查看 element() peek()
  • add(e)remove()element()不会阻塞线程。当不满足约束条件时,会抛出IllegalStateException。例如,当队列元素被填满后,再调用add(e)就会抛出异常。 >
  • 对于 BlockingQueue,想要实现阻塞功能,需要调用 put(e)take() 方法。

ConcurrentLinkedQueue 是基于链接节点的、无界的、线程安全的非阻塞队列。
在并发编程中一般推荐使用阻塞队列,这样实现可以尽量地避免程序出现意外的错误。阻塞队列使用的最经典场景是socket客户端数据的读取和解析,读取数据的线程不断将数据放入队列,然后解析线程不断从队列取数据解析。还有其他类似的场景,只有符合生产者-消费者模型都可以使用阻塞队列。
使用非阻塞队列,虽然能即时返回结果(消费结果),但必须自行编码解决返回为空的情况处理(以及消费重试等问题)。

Synchronized、volatile、Lock(ReentrantLocak)相关(⭐⭐⭐)

Synchronized的原理?