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执行的

    1. 最开始,我们编写的Java代码,是*.java文件
  1. 在编译(javac命令)后,从刚才的.java文件会变出一个新的Java字节码文件.class
  2. JVM会执行刚才生成的字节码文件(*.class),并把字节码文件转化为机器指令
  3. 机器指令可以直接在CPU上执运行,也就是最终的程序执行
  • JVM实现会带来不同的“翻译”,不同的CPU平台的机器指令又千差万别,无法保证并发安全的效果一致

面试题2:单例模式的作用和适用场景

  • 单例模式:只获取一次资源,全程序通用,节省内存和计算;保证多线程计算结果正确;方便管理;
    比如日期工具类只需要一个实例就可以,无需多个示例

面试题3:单例模式的写法,考察(重排序、单例和高并发的关系)

  • 饿汉式(静态常量、静态代码块)

    • 原理1:static静态常量在类加载的时候就初始化完成了,且由jvm保证线程安全,保证了变量唯一
    • 原理2:静态代码块中实例化和静态常量类似;放在静态代码块里初始化,类加载时完成;
    • 特征:简单,但没有懒加载(需要时再加载)
  • 懒汉式(加synchronized锁)

    • 对初始化的方法加synchronized锁达到线程安全的目的,但效率低,多线程下变成了同步
    • 懒汉式取名:用到的时候才去加载
  • 双重检查

    • 代码实现

      • 属性加volatile,两次if判断NULL值,第二次前加类锁
    • 优点

      • 线程安全;延迟加载;效率高
    • 为什么用双重而不用单层

      • 从线程安全方面、效率方面讲
  • 静态内部类

    • 需要理解静态内部类的优点,懒汉式加载,jvm加载顺序
  • 枚举

    • 代码实现简单

      • public enum Singleton{
        INSTANCE;
        public void method(){}
        }
    • 保证了线程安全

      • 枚举是一个特殊的类,经过反编译查看,枚举最终被编译成一个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),但是却始终得不到,可能原因是饥饿线程的优先级过低
posted @ 2020-06-19 21:00  fishers  阅读(675)  评论(0编辑  收藏  举报