这是我参与8月更文挑战的第7天,活动详情查看:8月更文挑战
Java并发已经是Java面试必问的一块内容,对这块知道越多的细节,就越容易让面试官刮目相看。
我结合自身学习和面试经历,总结了Java并发的相关面试题,包括线程基础、多线程与并发、线程安全、线程池、锁、内存模型、JUC、集合与并发这几块内容。
因篇幅限制,分成三篇文章
中篇:内存模型、锁
下篇:JUC、集合与并发
线程基础
什么是线程
线程(Thread)就是程序代码执行的一条线, 在Java代码层面看来, 是一个方法调用另一个方法,依次排列 的方法调用链。
当然,线程是操作系统中的概念,被称为轻量级的进程,是分配CPU资源和调度执行的基本单位。
什么是进程
进程(Process)是操作系统中的概念,是应用程序的一次动态执行过程,操作系统会给他分配各种资源, 比如内存,文件,以及CPU资源。
每个进程都有自己的内存空间,相对于静态的应用程序二进制代码来说,这个虚拟内存地址空间就是一 个副本。
比如,我们用命令行启动一次Java程序,就说启动了一个JVM进程。
线程与进程有什么区别?
一般来说,进程中可以包含多个线程,这些线程共享一块内存地址空间。
在Linux系统中,线程和进程概念并没有严格区分。
粗略来看,它们的区别有:
- 线程被称为轻量级的进程,线程之间的切换开销更小,线程占用的资源比进程少。
- 进程之间是独立的,不能共享内存地址空间;【Linux的轻量级进程我们当做线程来看即可】
Java中怎么创建线程?
Java语言中创建线程本质上只有一种方式: new Thread() 。 启动线程则是调用 start() 方法。
Java中,继承 Thread 类,实现 Runnable 接口,实现 Callable 接口,这些方式创建的都是可执行任 务,并没有真正地创建线程。
Thread#start() 和 Thread#run() 方法有什么区别?
Thread#start() : 启动一个新线程并异步执行其中的任务(真正创建了一个物理线程)。
Thread#run() : 在当前线程执行,和调用其他对象的普通方法没什么区别。
Thread类与Runnable接口有什么关系?
Thread类继承了Runnable接口,创建线程对象时,可以传入需要执行的 Runnable 任务。
Runnable 与 Callable 接口有什么区别?
Runnable#run() 没有返回值
Callable#call() 方法有返回值
线程有哪些状态?
Thread的状态包括:
- NEW:初始状态, 尚未启动
- RUNNABLE: 可运行状态
- RUNNING: 运行中
- READY: 就绪状态
- WAITING: 等待状态
- TIMED_WAITING: 限时等待被唤醒的状态
- BLOCKED: 阻塞状态,被对象锁或者IO阻塞
- TERMINATED: 终止状态
什么是守护线程?与前台线程的区别在哪里?
守护线程(Daemon Thread)也叫后台线程。
在JVM中,如果没有正在运行中的前台线程,则JVM就会自动结束运行,而不管守护线程。 所以守护线 程一般用于执行某些可以被放弃的任务或事件。
多线程与并发
并行和并发在你看来有什么区别?
concurrent: 并发,指多个线程在共同完成一件事情; 互相之间有依赖/有状态,例如多个部门做同 一个系统。
parallel: 并行,指多个线程各做各的事情; 互相之间无共享状态,例如两个公司,各做各的项目。
在GC算法中: concurrent指GC线程和业务线程一起执行的阶段; parallel则是指多个GC线程之间的并 行执行。
为什么需要多线程?
多线程是指程序中包含多个执行流,即在一个程序中可以同时运行多个不同的线程来执行不同的任务。
本质原因是摩尔定律失效,CPU进入多核时代。加上互联网时代的来临,分布式系统开发大规模普及。
多线程有什么优势?
多线程编程方式,通过合理的分工,能充分利用多个CPU核心,提高程序的执行性能。
再比如一个餐馆,多个服务员之间可以看做是多个并行线程。服务员和厨师之间则可以看多是多个并发 线程。
多线程有什么不好的地方?
- 多线程的程序更加复杂,开发成本更高;
- 消耗更多的资源,比如内存,CPU等等;
- 多线程需要协调和管理,会相互影响,有资源竞争问题。
如何让一个线程执行完再执行第二个?
- 使用 Thread#join() 方法,可以让当前线程阻塞, 等待指定的 thread 执行完成后,再执行当前 线程。
- 当前线程wait,直到指定线程执行完时执行notify通知唤醒当前线程执行。
- Lock和Condition也可以达到类似效果。
- Semaphore/CountDownLatch/CyclicBarrier都可以实现。
怎样让两个线程以指定顺序交替执行?
可以使用细粒度的锁(fine-grained locks)来控制执行顺序。
比如使用Java内置的 object.wait() 和 object.notify() 方法,依次执行完并通知对方。
或者使用同一个锁的多个 Condition, 分别等待。
或者创建自定义线程时, 使用 CountDownLatch 和 CyclicBarrier 等工具进行辅助。
Thread.sleep 和 Object#wait() 的区别
Thread.sleep() : 当前线程阻塞,让出CPU
Object#wait() : 当前线程进入等待状态,释放持有的锁
线程之间如何通信?
线程间通信(inter-thread communication)主要有两种方式:
- 共享内存: 多个线程之间使用堆内存之中的对象/属性作为状态值,来进行隐式的通信。
- 信号传递: 线程之间通过明确的发送信号来进行显式的通信。
线程安全
什么是线程安全?
线程安全是多线程环境下的一个概念,保证多个线程并发执行同一段代码时,不会出现不确定的结果, 也不会出现与单线程执行时不一致的结果。 也就是保证多个线程对共享状态操作的正确性。
在Java中,完全由代码来控制线程安全,共享状态一般是指堆内存中的数据(对象的属性)。
线程安全有哪些特征?
原子性: 对基本数据类型的变量的读取和赋值操作是原子性操作,即这些操作是不可被中断的,要 么执行,要么不执行。两个原子性的操作,先后执行,不能保证整体原子性。
可见性: 一个线程执行的修改操作,对其他线程来说必须立即可见。 Java 提供了volatile 关键字来 保证可见性,读取时强制从主内存读取。可见性不能解决原子性。
有序性: 保证线程内的串行语义,避免指令重排,例如增加内存屏障。
怎么保证线程安全?
- 使用原子类。
- 加锁: 例如 synchronized , Lock
- object.wait() 方法
- object.notify() 方法
- thread.join() 方法
- CountdownLatch 类
- CyclicBarrier 类
- FutureTask 类
- Callable 类
类加载和初始化的过程是线程安全的吗? 哪些情况下是不安全的?
类加载的过程是同步阻塞方式的,所以是线程安全的。
类和对象初始化的过程也是同步阻塞的,但如果初始化代码中有引用泄漏,则可能造成其他问题。
ThreadLocal 是什么?
ThreadLocal,线程本地变量,使得每一个调用的线程都能拥有一个跟其他线程隔离的变量。
ThreadLocal 的实现原理是什么?
每个 ThreadLocal 对象,为每个线程提供独立的变量副本,所以每个线程都可以独立地改变自己的副 本,而不会影响其它线程对应的副本。
ThreadLocal 有哪些使用场景?
- 维护遗留系统,避免增加方法调用参数,修改一连串方法签名
- Spring的JDBC连接以及事务管理
- 请求上下文: Tomcat基于线程的连接模型
使用 ThreadLocal 有哪些需要注意的地方?
- 注意防止污染:finally中及时进行清理,避免污染下一次的请求。
- 防止内存泄漏:避免将持有大量数据的对象放到ThreadLocal。
线程池
怎么创建线程池?
创建线程池的方式有多种,例如:
- 构造 ThreadPoolExecutor 对象
- 使用 Executors 工具类
有哪些种类的线程池
-
newSingleThreadExecutor 创建一个单线程的线程池。这个线程池只有一个线程在工作,也就是相当于单线程串行执行所有任 务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务 的执行顺序按照任务的提交顺序执行。
-
newFixedThreadPool 创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线 程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充 一个新线程。注意:workQueue无限制。
-
newCachedThreadPool 创建一个可缓存的线程池。如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空 闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此 线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线 程大小。
-
newScheduledThreadPool 创建一个大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。
创建线程池有哪些常用参数?
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 空闲存活时间
TimeUnit unit, // 空闲存活时间单位
BlockingQueue<Runnable> workQueue, // 工作队列; 排队队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略处理器
)
线程池在什么时候会创建新线程?
ThreadPoolExecutor 提交任务逻辑:
- 判断corePoolSize 【创建】
- 加入workQueue
- 判断maximumPoolSize 【创建】
- 执行拒绝策略处理器
线程池可以指定哪些拒绝策略?
- ThreadPoolExecutor.AbortPolicy : 丢弃任务并抛出 RejectedExecutionException 异常。 默认。
- ThreadPoolExecutor.DiscardPolicy : 丢弃任务,但是不抛出异常。
- ThreadPoolExecutor.DiscardOldestPolicy : 丢弃队列最前面的任务,然后重新提交被拒绝的 任务
- ThreadPoolExecutor.CallerRunsPolicy : 由调用线程(提交任务的线程)处理该任务。常用。
线程池都有哪些状态?
/**
* RUNNING -> SHUTDOWN
* On invocation of shutdown()
* (RUNNING or SHUTDOWN) -> STOP
* On invocation of shutdownNow()
* SHUTDOWN -> TIDYING
* When both queue and pool are empty
* STOP -> TIDYING
* When pool is empty
* TIDYING -> TERMINATED
* When the terminated() hook method has completed
*/
private static final int RUNNING = -1 << COUNT_BITS; // 运行中
private static final int SHUTDOWN = 0 << COUNT_BITS; // 关闭
private static final int STOP = 1 << COUNT_BITS; // 停止
private static final int TIDYING = 2 << COUNT_BITS; // 收拾
private static final int TERMINATED = 3 << COUNT_BITS; // 终止
线程池的 submit() 和 execute() 方法有什么区别?
- submit 方法: 有Future封装的返回值,执行中如果抛出异常,等待的方法中可以 catch 到。
- execute 方法: 无返回值,执行任务是捕捉不到异常的。
线程池有哪些关闭方法?
- shutdown() : 停止接收新任务,已有的任务继续执行。
- shutdownNow() : 停止接收新任务,停止执行已有的任务,正在执行的线程会抛出
- InterruptedException 异常。
- awaitTermination(long timeOut, TimeUnit unit) : 当前线程阻塞,等待终止。
使用线程池有哪些好处?
- 避免创建线程的开销。
- 避免线程数量爆炸,导致系统崩溃。
- 合理控制线程数量,避免过度的资源竞争,造成系统性能急剧下降。
- 利用特定线程池的功能特征,例如定时调度等。
线程池的实现原理是什么?
我们通过创建一个线程对象,并且实现Runnable接口就可以实现一个简单的线程。可以利用上多核 CPU。当一个任务结束,当前线程就接收。
但很多时候,我们不止会执行一个任务。如果每次都是如此的创建线程->执行任务->销毁线程,会造成 很大的性能开销。
那能否一个线程创建后,执行完一个任务后,又去执行另一个任务,而不是销毁。这就是线程池。
这也就是池化技术的思想,通过预先创建好多个线程,放在池中,这样可以在需要使用线程的时候直接 获取,避免多次重复创建、销毁带来的开销。
如果把线程池比作一个公司。公司会有正式员工处理正常业务,如果工作量大的话,会雇佣外包人员来 工作。
闲时就可以释放外包人员以减少公司管理开销。一个公司因为成本关系,雇佣的人员始终是有最大数。 如果这时候还有任务处理不过来,就走需求池排任务。
线程池创建参数如下:
- corePoolSize: 核心线程数量,可以类比正式员工数量,常驻线程数量。
- maximumPoolSize: 最大的线程数量,公司最多雇佣员工数量。常驻+临时线程数量。
- workQueue:多余任务等待队列,再多的人都处理不过来了,需要等着,在这个地方等。
- keepAliveTime:非核心线程空闲时间,就是外包人员等了多久,如果还没有活干,解雇了。
- threadFactory: 创建线程的工厂,在这个地方可以统一处理创建的线程的属性。每个公司对员工的要求不一样,恩,在这里设置员工的属性。
- handler:线程池拒绝策略,什么意思呢?就是当任务实在是太多,人也不够,需求池也排满了,还有任务咋办?默认是不处理,抛出异常告诉任务提交者,我这忙不过来了。
怎么提交任务?
提交一个任务到线程池中,线程池的处理流程如下: 1、判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创 建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。 2、线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。 如果工作队列满了,则进入下个流程。 3、判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果 已经满了,则交给饱和策略来处理这个任务。
怎么获取执行结果?
提交task到线程池后,可以获得Futrue对象,然后通过Future.get()获得执行结果。
如何控制线程池的线程池容量?
可以有如下策略:
- 如果创建时知道需要多少线程,可以使用newSingleThreadExecutor 或 newFixedThreadExecutor 创建单线程或固定大小线程。
- 如果不知道,可以使用newCachedThreadExecutor 创建无限制的线程池。
- 如果需要控制线程在一定范围内,可以直接使用ThreadPoolExecutor创建。
线程池怎样监控?
可以通过jstack,kill -3,jconsole/jvisualvm/jmc等工具监控。