目录
- Java多线程
- 面试题
- 线程和进程?
- 说说线程有几种创建方式?
- 为什么调用 start() 方法时会执行 run() 方法,那怎么不直接调用 run() 方法?
- 线程有哪些常用的调度方法?
- 线程有几种状态?
- 守护线程了解吗?
- 线程间有哪些通信方式?
- 并发和并行?
- 同步和异步?
- 为什么使用多线程?
- 使用多线程可能带来的问题?
- 什么是线程上下文切换?
- 什么是线程死锁?
- 死锁产生的条件?
- 如何避免死锁?
- 那死锁问题怎么排查呢?
- 活锁和饥饿锁了解吗?
- 线程间如何同步?
- sleep() 和 wait()?
- 为什么 wait() 方法不定义在 Thread 中?
- 说说你对原子性、可见性、有序性的理解?
- 如何保证变量的可见性?
- 那说说什么是指令重排?
- 指令重排有限制吗?happens-before 了解吗?
- as-if-serial 又是什么?单线程的程序一定是顺序的吗?
- volatile 关键字?
- volatile 实现原理了解吗?
- 解释双重校验锁实现单例的原理?
- volatile 可以保证原子性吗?
Java多线程
面试题
线程和进程?
进程(Process):
- 进程是操作系统分配资源的基本单位,是程序的一个执行实例
- 每个进程都有自己独立的内存空间,包括代码、数据、堆栈等
- 进程之间相互独立,一个进程的崩溃不会影响其他进程
- 进程间通信需要使用操作系统提供的机制,如管道、消息队列、共享内存等
线程(Thread):
- 线程是进程的一个执行流,是进程的一部分
- 线程共享进程的内存空间,因此线程间通信相对更容易
- 线程可以更轻量级地创建和销毁,开销小
联系与区别:
- 线程是进程的一部分,一个进程可以包含多个线程
- 进程是独立的执行环境,拥有独立的内存空间,而线程共享进程的内存空间
- 进程之间的切换开销较大,涉及上下文切换。线程切换开销相对较小
- 进程间通信通常需要操作系统提供的机制,而线程间通信相对更容易,可以通过共享内存等方式实现
说说线程有几种创建方式?
- 继承 Thread 类:重写 run() 方法,调用 start() 方法启动线程
- 实现 Runnable 接口:重写 run() 方法,调用 start() 方法启动线程
- 实现 Callable 接口:重写 call() 方法
- 使用匿名类或 Lambda 表达式
- 使用线程池
为什么调用 start() 方法时会执行 run() 方法,那怎么不直接调用 run() 方法?
- 调用 start() 方法会创建一个新线程,然后新线程执行 run() 方法,从而实现多线程并发运行
- 如果直接调用 run() 方法,就会在当前线程执行,不会启动新线程,就不会实现多线程并发执行
线程有哪些常用的调度方法?
- wait():使线程进入等待状态
- join():让线程等待另一个线程执行完成
- notify():唤醒等待中的一个线程
- notifyAll():唤醒所有等待中的线程
- yield():让当前线程让出 CPU 资源
- interrupt():中断线程
- isInterrupted():检查线程的中断状态
- sleep():使线程进入休眠状态
线程有几种状态?
![[image-20230329090159344.png]]
- 初始状态
- 运行状态
- 阻塞状态:需要等待锁释放
- 等待状态:线程需要等待其他线程做出一些特定动作 (通知或中断)
- 超时等待状态:可以在指定时间后自行返回
- 终止状态:线程已经运行完毕
守护线程了解吗?
Java 线程分为两类,分为守护线程和用户线程
- 守护线程:服务性质的线程,在所有用户线程结束时会自动结束。通常用来执行后台任务,比如垃圾回收、资源管理等。不影响 JVM 的退出
- 用户线程:用户手动创建的线程,在程序执行期间一直存在,直到主线程执行完成或者被手动终止。用户线程会影响 JVM 的退出
线程间有哪些通信方式?
- 共享变量:线程通过读写共享内存中的变量进行通信
- 锁机制:使用锁来限制对共享资源的访问,从而实现线程的同步和互斥
- wait-notify 机制:一个线程可以通过 wait 方法进入等待状态,等待其他线程调用 notify/notifyAll 来唤醒
- 使用管道流:管道流可以在两个线程之间进行数据传输
- 阻塞队列:一个线程可以通过向队列中添加元素来与其他线程进行通信
并发和并行?
- 并发:系统能够同时执行多个任务,不一定是同时执行,而是通过任务的切换和调度来实现多个任务交替执行
- 并行:系统中同时执行多个任务,每个任务在独立的处理器上执行,各个任务之间相互独立
同步和异步?
- 同步:调用一个函数或方法后,必须等待完成并返回结果,才能继续执行
- 异步:调用一个函数或方法后,不用等待完成,会通过回调函数、事件触发等机制来处理最终的结果
为什么使用多线程?
- 提高程序性能:在多核 CPU 上,多线程可以充分利用多核来并行处理任务,加快程序的执行速度
- 提高资源利用率:多线程可以充分利用 CPU 和内存等资源
- 异步处理:多线程可以实现异步操作,例如在后台加载数据、文件下载等,提高系统的响应性
使用多线程可能带来的问题?
- 内存泄露
- 死锁
- 线程不安全
- 性能问题
什么是线程上下文切换?
- 线程上下文切换是指在多线程环境中,由于多个线程共享 CPU 的执行时间
- 当一个线程的执行时间片用完或者需要切换到另一个线程时,操作系统会暂停当前线程的执行,保存其上下文,然后加载另一个线程的上下文,执行线程
什么是线程死锁?
- 死锁是多个线程因相互等待对方持有的资源而无法继续执行的僵局状态
死锁产生的条件?
- 互斥条件:资源只能被一个线程占用
- 请求并保持条件:已经获得资源的进程请求其他资源,同时不释放已有资源
- 不剥夺条件:已经占有的资源不能其他的线程抢占
- 循环等待条件:多个进程之间形成环路,每个进程都在等待下一个进程所占有的资源
如何避免死锁?
- 破坏死锁的四个必要条件
- 互斥条件:使用资源分级,避免资源互斥
- 请求与保持条件:线程在获取资源前释放已持有的资源
- 不剥夺条件:允许抢占已持有的资源
- 循环等待条件:强制线程按照一定的顺序获取资源
- 避免无限期等待
- 设置资源的超时时间,如果超时未获取,线程放弃或者重试
- 使用死锁检测和解除机制
- 定期检测系统中是否存在死锁
- 使用锁的顺序
- 确定锁的获取顺序,所有线程按照相同的顺序获取锁
那死锁问题怎么排查呢?
- 确认是否发生了死锁:查看日志,如果出现线程堵塞的异常情况可能是死锁问题
- 定位死锁位置:通过分析线程堆栈信息,定位出现死锁的代码位置
- 确认锁的粒度:确认锁的粒度是否过大,如果锁的粒度过大,容易出现死锁问题
- 检查锁的维护方式:检查锁是否正常释放等
- 分析并发访问情况:了解多线程请求锁的顺序和时间
- 制定解决方案:根据以上情况制定解决方案,如调整锁的粒度、优化代码逻辑、增加重试机制
- 测试验证:在实际系统中测试解决方案是否有效,如果没有解决问题,按照以上步骤继续排查
活锁和饥饿锁了解吗?
- 活锁:两个或多个线程都在互相等待对方执行某个操作,形成死循环
- 饥饿锁:某个线程一直无法获取需要的资源
线程间如何同步?
线程间的同步需要通过互斥锁、条件变量和信号量等机制来实现
- 互斥锁:
- 互斥锁是一种最基本的同步机制,可以实现对共享资源的互斥访问。
- 当某个线程进入临界区进行访问时,其他线程需要等待,直到该线程离开临界区,释放锁资源,才能再继续访问共享资源
- 条件变量:
- 条件变量是一种用于线程间等待和通知的机制
- 当某个线程进入临界区后,如果发现共享资源不满足当前需求,那它就可以将自己休眠,释放锁资源,等待其他线程改变共享资源状态并发送通知以后,再重新唤醒
- 信号量:
- 信号量是一种更高级的线程同步机制,可以实现多个线程之间的同步操作。
- 当某个线程需要访问共享资源时,请求获取信号量。如果有其他线程正在使用该共享资源,则该线程进入休眠状态等待信号量被释放。当共享资源被释放时,信号量发出通知,并将等待资源的线程唤醒
sleep() 和 wait()?
在多线程中,sleep() 和 wait() 方法都可以暂停线程的执行
- sleep() 方法不会释放锁,wait() 会释放锁
- sleep() 方法通常用于暂停执行,wait() 通常用于线程间通信
- sleep() 方法在暂停指定时间后,自动恢复运行;wait() 方法调用后,需要其他线程调用 notify/notifyAll 方法唤醒
- sleep() 方法是 Thread 类方法,wait() 是 Object 类方法
为什么 wait() 方法不定义在 Thread 中?
- wait() 方法用于线程间通信,通过释放对象的锁并让线程进入等待状态,每个对象都拥有对象锁,因此定义在 Object 类
- sleep() 方法是让当前线程暂停执行,不涉及对象类,因此定义在 Thread 类中
说说你对原子性、可见性、有序性的理解?
- 原子性:
- 原子性指一个操作不可分割的,要么完全执行,要么完全不执行
- 原子性确保了操作的不可分割性
- 可见性:
- 可见性指一个线程对共享数据进行修改后,其他线程立即能知道这个修改
- 可见性确保了线程间共享数据的同步性
- 有序性:
- 有序性指程序执行的顺序和代码顺序一致
如何保证变量的可见性?
- 使用 volatile 关键字:将变量声明为 volatile,每次访问该变量时都会从主存中读取最新的值,而不是使用线程的缓存
- 使用 synchronized 或 Lock:使用同步机制来确保对变量的访问时互斥的
- 使用原子类:原子类的操作是原子的,可以保证对变量的修改可见
- 使用并发集合:使用线程安全的并发集合类,如 ConcurrentHashMap
- 使用线程间通信:可以使用线程间通信机制,确保线程间的操作顺序和可见性
那说说什么是指令重排?
- 指令重排是处理器和编译器的一种优化手段
- 通过优化指令的执行顺序,充分利用处理器的执行单元,减少指令的等待时间,提高程序的执行效率
指令重排有限制吗?happens-before 了解吗?
- 指令重排有限制,有两个规则 happens-before 和 as-if-serial
- happens-before 规则用来定义多线程间操作的执行顺序
如果操作 A happens-before 操作 B,那么在程序执行中,A 的效果对 B 可见
as-if-serial 又是什么?单线程的程序一定是顺序的吗?
- as-if-serial 规则是 Java 内存模型的另一个重要原则
允许虚拟机在保证多线程正确性的前提下进行指令重排,只要重排后的执行结果和原始执行顺序一致即可 - 单线程的程序一定是顺序执行的
volatile 关键字?
volatile 关键字可以保证可见性和有序性
- 保证可见性:线程读取变量的值都从主存中读取
- 保证有序性:volatile 关键字可以禁止指令重排
volatile 实现原理了解吗?
- volatile 实现可见性和有序性的关键在于内存屏障
- 内存屏障是一种硬件或软件层面的机制,可以确保在特定位置的操作不会发生在内存屏障之前或之后
- volatile 变量的读写操作会插入内存屏障,保证读写 volatile 变量时不会指令重排
解释双重校验锁实现单例的原理?
双重校验锁是一种常用于实现线程安全的懒汉式单例模式的优化方式
- 首次检查:在双重校验锁中,首先检查对象实例是否被创建,如果没有才进行同步
- 同步块:在单例对象还没有创建的时候,使用synchronized对代码块加锁,并通过两次检查对象是否被实例化,保证只有一个线程执行了同步块内的代码
- volatile 关键字:使用 volatile 关键字确保线程之间的可见性和禁止指令重排序,避免出现另一个线程获得不完全初始化的实例
//双重校验锁实现对象单例(线程安全)
public class Singleton {
private volatile static Singleton uniqueInstance;
private Singleton() {
}
public static Singleton getUniqueInstance() {
//先判断对象是否已经实例过,没有实例化过才进入加锁代码
if (uniqueInstance == null) {
//类对象加锁
synchronized (Singleton.class) {
if (uniqueInstance == null) {
uniqueInstance = new Singleton();
}} }
return uniqueInstance;}}
volatile 可以保证原子性吗?
- volatile 关键字不能保证对变量的操作是原子性的
秋招后端开发面试题系列目录
一、Java
1.1 Java基础上
1.2 Java基础下
1.3 Java集合
1.4 JavaIO
1.5 Java多线程上
1.6Java多线程下
二、JVM
2.1 JVM底层原理
2.2 垃圾回收器
2.3 垃圾回收算法
2.4 类加载机制
2.5 运行时数据区
三、MySQL
3.1 MySQL基础
3.2 事务
3.3 索引
3.4 锁机制
3.5 MVCC
四、Redis
4.1 Redis基础
4.2 缓存原理
五、中间件
5.1 RabbitMQ
六、Spring开源框架
6.1 Spring
6.2 Spring MVC
6.3 Spring Boot
6.4 MyBatis
七、操作系统
八、计算机网络
九、设计模式
十、微服务架构
十一、Spring Cloud分布式
11.1 分布式基础
11.2 Spring Cloud
11.3 GateWay
11.4 Nacos
11.5 OpenFeign
11.6 Ribbon
十二、算法
十三、项目