Java并发常见面试题(上)
什么是线程和进程?
一个 Java 程序的运行是 main 线程和多个其他线程同时运行
进程:程序的一次执行过程,系统运行一个程序就是一个进程从创建,运行到消亡的过程。在Java中启动main函数就是开启一个进程,main函数所在线程就是主线程
线程:一个进程可以产生多个线程。与进程不同的是同类的多个线程共享进程的堆和方法区。每个线程有自己的程序计数器,虚拟机和本地方法栈
Java进程和操作系统的进程有啥区别?
JDK1.2之前Java是绿色线程,也就是自己模拟多线程运行不依赖操作系统,但是绿色线程不能使用操作系统提供的功能(异步IO)且无法利用多核.在之后就改为原生线程
- 用户线程:由用户空间程序管理和调度的线程,运行在用户空间(专门给应用程序使用)。
- 内核线程:由操作系统内核管理和调度的线程,运行在内核空间(只有内核程序可以访问)
现在的 Java 线程的本质其实就是操作系统的线程。
线程和进程关系和区别
一个进程有多个线程,多个线程共享进程的堆和方法区。每个线程有自己的程序计数器,虚拟机栈和本地方法栈
为什么程序计数器,虚拟机栈和本地方法栈是私有的?
程序计数器是通过解释器来控制代码执行,为了方便切换线程后恢复正确执行位置所以是私有的
虚拟机栈存放方法创建的局部变量,常量池等。本地方法栈为虚拟机执行的native方法服务
所以为了保护局部变量不被其他线程访问,虚拟机栈和本地方法栈是私有的
了解堆和方法区
堆存放新建的对象,方法区存放类信息,常量,静态变量等数据
如何创建线程?
一般来说,创建线程有很多种方式,例如继承Thread
类、实现Runnable
接口、实现Callable
接口、使用线程池、使用CompletableFuture
类等等。
严格来说,Java 就只有一种方式可以创建线程,那就是通过new Thread().start()
创建。不管是哪种方式,最终还是依赖于new Thread().start()
。
线程生命周期和状态
- NEW: 初始状态,线程被创建出来但没有被调用
start()
。 - RUNNABLE: 运行状态,线程被调用了
start()
等待运行的状态。 - BLOCKED:阻塞状态,需要等待锁释放。
- WAITING:等待状态,表示该线程需要等待其他线程做出一些特定动作(通知或中断)。
- TIME_WAITING:超时等待状态,可以在指定的时间后自行返回而不是像 WAITING 那样一直等待。
- TERMINATED:终止状态,表示该线程已经运行完毕。
什么是线程上下文切换
线程切换需要保存当前线程的上下文
线程在执行过程中有自己的运行条件和状态(上下文)
线程上下文切换就是保留此线程的上下文,加载下一个占用cpu的上下文
Thread.sleep和wait方法的区别
共同点:两者使线程暂停
区别:
- sleep方法没有释放锁,wait释放锁
- wait方法用于切换线程,sleep用于暂停执行
- wait方法苏醒需要notify或者notifyall方法,sleep执行后自动苏醒
- sleep是Thread的静态本地方法,wait是object的本地方法
为什么wait在Object中?
因为它需要得到对象锁,而不是线程
sleep就没有关于对象的操作所以在Thread中
可以直接调用Thread类的run方法吗?
new 一个Thread,线程进入准备状态,调用start方法会启动一个线程进入就绪状态分配到时间片就开始执行了,会自动调用run方法
总结:调用 start()
方法方可启动线程并使线程进入就绪状态,直接执行 run()
方法的话不会以多线程的方式执行。
并发与并行的区别
并发:同一时间段执行任务
并行:同一时刻之星任务
同步和异步的区别
- 同步:发出一个调用之后,在没有得到结果之前, 该调用就不可以返回,一直等待。
- 异步:调用在发出之后,不用等待返回结果,该调用直接返回
为什么要使用多线程?
多核时代减少了线程上下文切换的开销,多线程机制可以提高性能
单核CPU支持多线程
操作系统通过时间片轮转的方式,将 CPU 的时间分配给不同的线程。尽管单核 CPU 一次只能执行一个任务,但通过快速在多个线程之间切换,可以让用户感觉多个任务是同时进行的。
系统调度方式
- 抢占式调度:系统时钟中断(时间片轮转)或其他高优先级事件(如 I/O 操作完成)触发的。存在上下文开销,不易阻塞
- 协同式调度:线程结束后开始下一个线程,减少了上下文开销,容易阻塞
Java 使用的线程调度是抢占式的,JVM 本身不负责线程的调度,而是将线程的调度委托给操作系统。操作系统通常会基于线程优先级和时间片来调度线程的执行,高优先级的线程通常获得 CPU 时间片的机会更多
单核CPU上运行多个线程效率会高吗?
两种情况:
- CPU集密型:占用大部分CPU资源,降低了效率
- IO集密型:大量IO操作,不占用CPU资源,提高了效率
多线程带来的问题
内存泄漏,死锁,线程不安全
如何理解线程不安全和安全
对同一份数据的访问能否保证正确性和一致性
什么是线程死锁
多个线程等待一个资源被释放,但是这个资源被无限期地阻塞了
如下图所示,线程 A 持有资源 2,线程 B 持有资源 1,他们同时都想申请对方的资源,所以这两个线程就会互相等待而进入死锁状态。
死锁的四个必要条件:
- 互斥条件:该资源任意一个时刻只由一个线程占用。
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
- 不剥夺条件:线程已获得的资源在未使用完之前不能被其他线程强行剥夺,只有自己使用完毕后才释放资源。
- 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系
如何检测死锁
- 使用
jmap
、jstack
等命令查看 JVM 线程栈和堆内存的情况。如果有死锁,jstack
的输出中通常会有Found one Java-level deadlock:
的字样,后面会跟着死锁相关的线程信息。另外,实际项目中还可以搭配使用top
、df
、free
等命令查看操作系统的基本情况,出现死锁可能会导致 CPU、内存等资源消耗过高。 - 采用 VisualVM、JConsole 等工具进行排查
如何预防和避免死锁
如何预防死锁? 破坏死锁的产生的必要条件即可:
- 破坏请求与保持条件:一次性申请所有的资源。
- 破坏不剥夺条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
- 破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。
如何避免死锁?
避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。
安全状态 指的是系统能够按照某种线程推进顺序(P1、P2、P3……Pn)来为每个线程分配所需资源,直到满足每个线程对资源的最大需求,使每个线程都可顺利完成。称 <P1、P2、P3.....Pn>
序列为安全序列。