Java多线程与并发基础面试题
CS-LogN思维导图:记录专业基础 面试题
开源地址:https://github.com/FISHers6/CS-LogN
多线程与并发基础
实现多线程
面试题1:有几种实现线程的方法,分别是什么
-
1.继承Thread类,启动线程的唯一方法就是通过 Thread 类的 start()实例方法,start()方法是一个 native 方法,它将启动一个新线程去执行 run()方法
-
2.实现 Runnable 接口,重写run()函数,作为参数放到Thread类构造函数中作为target属性,运行start()方法
-
线程池创建线程、Callable本质还是使Runnable创建,Callable是父辈类继承了Runnable,线程池需传入参数
面试题2:实现Runnable方法好,还是继承Thread类好
-
实现Runnable接口更好
- 1.单一继承原则,如果继承了Thread,就不能继承其它类了,限制了可扩展性
- 2.Thread类每次只能创建一个独立的线程,损耗大,而Runnable能利用线程池工具来创建线程
- 3.从代码架构上看,run内容应该与Trhead代码解耦
面试题3:一个线程两次调用start方法会出现什么情况(考察源码)
- 第二次会出现异常,从start源码上和线程生命周期上分析,一个线程start后,
改变了threadState状态字;而第二次再start每次会先检查这个状态不是0就报异常
面试题4:既然start方法会调用run方法,为什么我们还是要用start方法,而不是直接调用run方法呢(考察源码)
- 因为start后线程才会经过完整的线程生命周期,start调用native start0,虚拟机执startThread,thread_entry入口中调用Thread的run,
面试题5:start和run有什么区别
- run()方法:只是普通的方法,调用run普通方法,可以重复多次调用
- start()方法,会启动一个线程,使得虚拟机去调用Runnable对象的run()方法,不能多次启动同一个线程
面试题6:start方法如何调用run方法的(考察源码和JVM)
- start方法调用native start0,JVM虚拟机执行startThread,在thread_entry中调用Thread的run方法
面试题7:如何正确停止线程
- 使用interrupt中断通知,而不是强制,中断通知后会让被停止线程去决定何时停止,即把主动权交给需要被中断的线程
线程的生命周期
面试题1:Java线程有哪几种状态 说说生命周期
-
六种生命状态(若time_waiting也算一种)
- New,已创建但还尚未启动的新线程
- Runable,可运行状态;对应操作系统的两种状态“就绪态” 和 “运行态”(分配到CPU)
- Blocked,阻塞状态;请求synchronized锁未分配到时阻塞,直到获取到monitor锁再进入Runnable
- Waiting,等待状态
- Timed waiting,限期等待
- Terminated终止状态
-
线程的生命周期 状态转换图
Thread和Object类中
与线程相关的重要方法
面试题1:实现两个线程交替打印奇数偶数
面试题2:手写生产者消费者设计模式,为什么用该模式
- 主要是为了解耦,匹配不同的能力
面试题3:wait后发生了什么,为什么需要在同步代码内才能使用
- 从jvm的源码实现上看,wait后,线程让出占有的cpu并释放同步资源锁;把自己加入到等待池,以后不会再主动参与cpu的竞争,除非被其它notify命中
- 为了确保线程安全;另外wait会释放资源,所以肯定要先拿到这个锁,能进入同步代码块已经拿到了锁
面试题4:为什么线程通信的方法wait,notify和notifyAll放在Object类,而sleep定义在Thread类里 (考察对象锁)
- 与对象的锁有关,对象锁绑定在对象的对象头中,且放在Object里,使每个线程都可以持有多个对象的锁
面试题5:wait方法是属于Object对象的,那调用Thread.wait会怎么样
- 线程死的时候会自己notifyAll,释放掉所有的持有自己对象的锁。这个机制是实现很多同步方法的基础。如果调用Thrad.wait,干扰了我们设计的同步业务流程
面试题6:如何选择notify还是notifyAll
- 优先选用notifyAll,唤醒所有线程;除非业务需要每次只唤醒一个线程的
面试题7:notfiy后发生的操作,notifyAll之后所有的线程都会再次抢夺锁,如果某线程抢夺失败怎么办?
- notify后,让waiterSet等待池中的一个线程与entry_List锁池一级活跃线程一起竞争CPU
- 抢夺锁失败后会继续待在原锁池或原等待池,等待竞争CPU的调度
面试题8:sleep方法与notify/wait方法的异同点
- 相同点:线程都会进入waiting状态,都可以响应中断
- 不同点:1.所属类不同;2.wait/notify必须用在同步方法中,且会释放锁;3.sleep可以指定时间
面试题9:join方法后父线程进入什么状态
- waiting状态,join内部调用wait,子线程结束后自动调用notifyAll唤醒(jvm:exit函数)
线程安全与性能
面试题1:守护线程和普通线程的区别
- 守护线程是服务于普通线程的,并且不会影响到jvm的退出
面试题2:什么是线程安全
- 不管业务中遇到怎样的多个线程访问某对象或某方法的情况,而在编程这个业务逻辑的时候,都不需要再额外做任何额外的处理(也就是可以像单线程编程一样),程序也可以正常运行(不会因为多线程而出错),就可以称为线程安全
面试题3:有哪些线程不安全的情况,什么原因导致的
- 1.数据争用、同时操作,如数据读写由于同时写,非原子性操作导致运行结果错误,a++
- 2.存在竞争,顺序不当,如死锁、活锁、饥饿
面试题4:什么是多线程的上下文切换,及导致的后果
- 进程线程切换要保存所需要的CPU运行环境,如寄存器、栈、全局变量等资源
- 在频繁的io以及抢锁的时候,会导致密集的上下文切换,多线程切换时,由于缓存和上下文的切换会带来性能问题
面试题5:多线程导致的开销有哪些
-
1.上下文切换开销,如保存缓存(cache、快表等)的开销
-
2.同步协作的开销(java内存模型)
- 为了数据的正确性,同步手段往往会使用禁止编译器优化(如指令重排序优化、锁粗化等),性能变差
- 使CPU内的缓存失效(比如volatile可见性让自己线程的缓存失效后,必须使用主存来查看数据)
Java内存模型
面试题1:Java的代码如何一步步转化,最终被CPU执行的
-
- 最开始,我们编写的Java代码,是*.java文件
- 在编译(javac命令)后,从刚才的.java文件会变出一个新的Java字节码文件.class
- JVM会执行刚才生成的字节码文件(*.class),并把字节码文件转化为机器指令
- 机器指令可以直接在CPU上执运行,也就是最终的程序执行
- JVM实现会带来不同的“翻译”,不同的CPU平台的机器指令又千差万别,无法保证并发安全的效果一致
面试题2:单例模式的作用和适用场景
- 单例模式:只获取一次资源,全程序通用,节省内存和计算;保证多线程计算结果正确;方便管理;
比如日期工具类只需要一个实例就可以,无需多个示例
面试题3:单例模式的写法,考察(重排序、单例和高并发的关系)
-
饿汉式(静态常量、静态代码块)
- 原理1:static静态常量在类加载的时候就初始化完成了,且由jvm保证线程安全,保证了变量唯一
- 原理2:静态代码块中实例化和静态常量类似;放在静态代码块里初始化,类加载时完成;
- 特征:简单,但没有懒加载(需要时再加载)
-
懒汉式(加synchronized锁)
- 对初始化的方法加synchronized锁达到线程安全的目的,但效率低,多线程下变成了同步
- 懒汉式取名:用到的时候才去加载
-
双重检查
-
代码实现
- 属性加volatile,两次if判断NULL值,第二次前加类锁
-
优点
- 线程安全;延迟加载;效率高
-
为什么用双重而不用单层
- 从线程安全方面、效率方面讲
-
-
静态内部类
- 需要理解静态内部类的优点,懒汉式加载,jvm加载顺序
-
枚举
-
代码实现简单
- public enum Singleton{
INSTANCE;
public void method(){}
}
- public enum Singleton{
-
保证了线程安全
- 枚举是一个特殊的类,经过反编译查看,枚举最终被编译成一个final的类,继承了枚举父类。各个实例通过static定义,本质就是一个静态的对象,所有第一次使用的时候采取加载(懒加载)
-
避免反序列化破坏单例
- 避免了:比如用反射就绕过了构造方法,反序列化出多个实例
-
面试题4:单例模式各种写法分别适用的场合
- 1.最好的方法是枚举,因枚举被编译成final类,用static定义静态对象,懒加载。既保证了线程安全又避免了反序列化破坏单例
- 2.如果程序一开始要加载的资源太多,考虑到启动速度,就应该使用懒加载
- 3.如果是对象的创建需要配置文件(一开始要加载其它资源),就不适合用饿汉式
面试题5:饿汉式单例的缺点
- 没有懒加载(初始化时全部加载出),初始化开销大
面试题6:懒汉式单例的缺点
- 虽然用到的时候才去加载,但是由于加锁,性能低
面试题7:单例模式的双重检查写法为什么要用double-check
- 从代码实现出发,保证线程安全、延迟加载效率高
面试题8:为什么双重检查要用volatile
-
1.保证instance的可见性
- 类初始化分成3条指令,重排序带来NPE空虚指针问题,加volatile防止重排序
-
2.防止初始化指令重排序
面试题9:讲一讲什么是Java的内存模型
- 1.是一组规范,需要JVM实现遵守这个规范,以便实现安全的多线程程序
2.volatile、synchronized、Lock等同步工具和关键字实现原理都用到了JMM
3.重排序、内存可见性、原子性
面试题10:什么是happens-before,规则有哪些
-
解决可见性问题的:在时间上,动作A发生在动作B之前,B保证能看见A,这就是happens-before
-
规则
- 1 单线程按代码顺序规则;2 锁操作(synchronized和Lock);3volatile变量;4.JUC工具类的Happens-Before原则
- 5.线程启动时子线程启动前能看到主线程run的所有内容;6.线程join主线程一定要等待子线程完成后再去做后面操作
- 7.传递性 8.中断检测 9.对象构造方法的最后一行指令 happens-before 于 finalize() 方法的第一行指令
面试题11:讲一讲volatile关键字
- volatile是一种同步机制,比synchronized或者Lock相关类更轻量,因为使用volatile并不会发生上下文切换等开销很大的行为。而加锁时对象锁会阻塞开销大。
- 可见性,如果一个变量别修饰成volatile,那么JVM就知道了这个变量可能会被并发修改;
- 不能保证原子性
面试题12:volatile的适用场合及作用
-
作用
- 1.保证可见性 2.禁止指令重排序(单例双重锁时)
-
适合场景
- 适用场合1:boolean flag,布尔具有原子性,可再由volatile保证其可见性
- 适用场合2:作为刷新之前变量的触发器
- 但不适合非原子性操作如:a++等
面试题13:volatile和synchronized的异同
- 1 性能开销方面: 锁开销更大,volatile无加锁阻塞开销
2 作用方面:volatile只能保证可见性,锁既能保证可见性,又能保证原子性
面试题14:什么是内存可见性问题,为什么存在
- 多线程下,一个线程修改共享数据后,其它线程能否感知到修改了数据的线程的变化
- CPU有多级缓存,导致读的数据过期,各处理机有独自的缓存未及时更新时,与主存内容不一致
面试题15:主内存和本地内存的关系是什么
- Java 作为高级语言,屏蔽了CPU cache等底层细节,用 JMM 定义了一套读写内存数据的规范,虽然我们不再需要关心一级缓存和二级缓存的问题,但是,JMM 抽象了主内存和本地内存的概念。
- 线程拥有自己的本地内存,并共享主内存的数据;线程读写共享数据也是通过本地内存交换的,所以才导致了可见性问题。
面试题16:什么是原子操作,Java的原子操作有哪些
-
原子操作
- 一系列的操作,要么全部执行成功,要么全部不执行,不会出现执行一半的情况,是不可分割的。
-
1)除long和double之外的基本类型(int, byte, boolean, short, char, float)的”赋值操作”
-
2)所有”引用reference的赋值操作”,不管是 32 位的机器还是 64 位的机器
-
3)java.concurrent.Atomic.* 包中所有类的原子操作
面试题17:long 和 double 的原子性你了解吗
- 在32位上的JVM上,long 和 double的操作不是原子的,但是在64位的JVM上是原子的。
- 在32位机器上一次只能读写32位;而浮点数、long型有8字节64位;要分高32位和低32位两条指令分开写入,类似汇编语言浮点数乘法分高低位寄存器;64位不用分两次读写了
面试题18:生成对象的过程是不是原子操作
- 不是,对象生成会生成分配空间、初始化、赋值,三条指令,有可能会被重排序,导致空指针
面试题19:区分JVM内存结构、Java内存模型 、Java对象模型
-
Java内存模型,和Java的并发编程有关
- 详见面试题9
-
JVM内存结构,和Java虚拟机的运行时区域(堆栈)有关
-
堆区、方法区(存放常量池 引用 类信息)
栈区、本地方法栈、程序计数器
-
-
Java对象模型,和Java对象在虚拟机中的表现形式有关
- 是Java对象自身的存储模型,在方法区中Kclass类信息(虚函数表),在堆中存放new实例,在线程栈中存放引用,OOP-Klass Model
面试题20:什么是重排序
- 指令实际执行顺序和代码在java文件中的顺序不一致
- 重排序的好处:提高处理速度,包括编译器优化、指令重排序(局部性原理)
死锁
面试题1:写一个必然死锁的例子
- synchronized嵌套,构成请求循环
面试题2:生产中什么场景下会发生死锁
- 并发中多线程互不相让:当两个(或更多)线程(或进程)相互持有对方所需要的资源,又不主动释放,导致所有人都无法继续前进,导致程序陷入无尽的阻塞,这就是死锁。
面试题3:发生死锁必须满足哪些条件
- 1.互斥
- 2.请求和保持
- 3.不可剥夺
- 4.存储循环等待链
面试题4:如何用工具定位死锁
- 1.jstack命令在程序发生死锁后,进行堆栈分析出死锁线程
- 2.ThreadMXbean 程序运行中发现死锁,一旦发现死锁可以让用户去打日志
面试题5:有哪些解决死锁问题的策略
-
1.死锁语法,不让死锁发生
- 破坏死锁的四个条件之一;如:哲学家换手、转账换序
-
2.死锁避免
- 银行家算法、系统安全序列
-
3.死锁检查与恢复
- 适用资源请求分配图,一段时间内检查死锁,有死锁就恢复策略,采用恢复策略;
- 恢复方法:进程终止 、资源剥夺
-
4.鸵鸟策略(忽略死锁)
- 先忽略,后期再让人工恢复
面试题6:死锁避免策略和检测与恢复策略的主要思路是什么
-
死锁语法
- 破坏死锁的四大条件之一
-
死锁避免
- 找到安全序列,银行家算法
-
死锁检测与恢复
- 资源请求分配图
面试题7:讲一讲经典的哲学家就餐问题,如何解决死锁
-
什么时候死锁
- 哲学家各拿起自己左手边的筷子,又去请求拿右手边筷子循环请求时而阻塞
-
如何解决死锁
- 1.一次两只筷子,形成原子性操作
- 2.只允许4个人拿有筷子
面试题8:实际开发中如何避免死锁
- 设置超时时间
- 多使用并发类而不是自己设计锁
- 尽量降低锁的使用粒度:用不同的锁而不是一个锁,锁的范围越小越好
- 避免锁的嵌套:MustDeadLock类
- 分配资源前先看能不能收回来:银行家算法
- 尽量不要几个功能用同一把锁:专锁专用
- 给你的线程起个有意义的名字:debug和排查时事半功倍,框架和JDK都遵守这个最佳实践
面试题9:什么是活跃性问题?活锁、饥饿和死锁有什么区别
-
活锁
-
虽然线程并没有阻塞,也始终在运行(所以叫做“活”锁,线程是“活”的),但是程序却得不到进展,因为线程始终互相谦让,重复做同样的事
-
工程中的活锁实例:消息队列,消息如果处理失败,就放在队列开头重试,没阻塞程序无法继续
-
如何解决活锁问题
- 加入随机因素,以太网的指数退避算法
-
-
饥饿
- 当线程需要某些资源(例如CPU),但是却始终得不到,可能原因是饥饿线程的优先级过低