思维导图

面试题

Q1:JMM 的作⽤是什么?

Java 线程的通信由 JMM 控制,JMM 的主要⽬的是定义程序中各种变量的访问规则。变量包括实例字段、静态字段,但不包括局部变量与⽅法参数,因为它们是线程私有的,不存在多线程竞争。JMM 遵循⼀个基本原则:只要不改变程序执⾏结果,编译器和处理器怎么优化都⾏。例如编译器分析某个锁只会单线程访问就消除锁,某个 volatile 变量只会单线程访问就把它当作普通变量。
JMM 规定所有变量都存储在主内存,每条线程有⾃⼰的⼯作内存,⼯作内存中保存被该线程使⽤的变量的主内存副本,线程对变量的所有操作都必须在⼯作空间进⾏,不能直接读写主内存数据。不同线程间⽆法直接访问对⽅⼯作内存中的变量,线程通信必须经过主内存。
关于主内存与⼯作内存的交互,即变量如何从主内存拷⻉到⼯作内存、从⼯作内存同步回主内存,JMM定义了 8 种原⼦操作:

操作 作⽤变量范围 作⽤
lock 主内存 把变量标识为线程独占状态
unlock 主内存 释放处于锁定状态的变量
read 主内存 把变量值从主内存传到⼯作内存
load ⼯作内存 把 read 得到的值放⼊⼯作内存的变量副本
use ⼯作内存 把⼯作内存中的变量值传给执⾏引擎
assign ⼯作内存 把从执⾏引擎接收的值赋给⼯作内存变量
store ⼯作内存 把⼯作内存的变量值传到主内存
write 主内存 把 store 取到的变量值放⼊主内存变量中


Q2:as-if-serial 是什么?

不管怎么重排序,单线程程序的执⾏结果不能改变,编译器和处理器必须遵循 as-if-serial 语义。
为了遵循 as-if-serial,编译器和处理器不会对存在数据依赖关系的操作重排序,因为这种重排序会改变执⾏结果。但是如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。as-if-serial 把单线程程序保护起来,给程序员⼀种幻觉:单线程程序是按程序的顺序执⾏的。


Q3:happens-before 是什么?

先⾏发⽣原则,JMM 定义的两项操作间的偏序关系,是判断数据是否存在竞争的重要⼿段。
JMM 将 happens-before 要求禁⽌的重排序按是否会改变程序执⾏结果分为两类。对于会改变结果的重排序 JMM 要求编译器和处理器必须禁⽌,对于不会改变结果的重排序,JMM 不做要求。
JMM 存在⼀些天然的 happens-before 关系,⽆需任何同步器协助就已经存在。如果两个操作的关系不在此列,并且⽆法从这些规则推导出来,它们就没有顺序性保障,虚拟机可以对它们随意进⾏重排序。

  • **程序次序规则:**⼀个线程内写在前⾯的操作先⾏发⽣于后⾯的。
  • **管程锁定规则: **unlock 操作先⾏发⽣于后⾯对同⼀个锁的 lock 操作。
  • **volatile 规则:**对 volatile 变量的写操作先⾏发⽣于后⾯的读操作。
  • **线程启动规则:**线程的 start ⽅法先⾏发⽣于线程的每个动作。
  • **线程终⽌规则:**线程中所有操作先⾏发⽣于对线程的终⽌检测。
  • **对象终结规则:**对象的初始化先⾏发⽣于 finalize ⽅法。
  • **传递性:**如果操作 A 先⾏发⽣于操作 B,操作 B 先⾏发⽣于操作 C,那么操作 A 先⾏发⽣于操作C 。

Q4:as-if-serial 和 happens-before 有什么区别?

as-if-serial 保证单线程程序的执⾏结果不变,happens-before 保证正确同步的多线程程序的执⾏结果不变。
这两种语义的⽬的都是为了在不改变程序执⾏结果的前提下尽可能提⾼程序执⾏并⾏度。

Q5:什么是指令重排序?

为了提⾼性能,编译器和处理器通常会对指令进⾏重排序,重排序指从源代码到指令序列的重排序,分为三种:

  • ① 编译器优化的重排序,编译器在不改变单线程程序语义的前提下可以重排语句的执⾏顺序。
  • ② 指令级并⾏的重排序,如果不存在数据依赖性,处理器可以改变语句对应机器指令的执⾏顺序。
  • ③ 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

Q6:原⼦性、可⻅性、有序性分别是什么?

原⼦性

原子性是指一个操作不能被打断,要么全部执行完毕,要么不执行。在这点上有点类似于事务操作,要么全部执行成功,要么回退到执行该操作之前的状态。
基本数据类型的访问都具备原⼦性,例外就是 long 和 double,long 和double类型的变量是64位,但是在32位JVM中,32位的JVM会将64位数据的读写操作分为2次32位的读写操作来进行,这就导致了long、double类型的变量在32位虚拟机中是非原子操作,数据有可能会被破坏,也就意味着多个线程在并发访问的时候是线程非安全的。
如果应⽤场景需要更⼤范围的原⼦性保证,JMM 还提供了 lock 和 unlock 操作满⾜需求,尽管 JVM 没有把这两种操作直接开放给⽤户使⽤,但是提供了更⾼层次的字节码指令 monitorenter 和monitorexit,这两个字节码指令反映到 Java 代码中就是 synchronized。

可⻅性

可⻅性指当⼀个线程修改了共享变量时,其他线程能够⽴即得知修改。JMM 通过在变量修改后将值同步回主内存,在变量读取前从主内存刷新的⽅式实现可⻅性,⽆论普通变量还是 volatile 变量都是如此, 区别是 volatile 保证新值能⽴即同步到主内存以及每次使⽤前⽴即从主内存刷新。
除了 volatile 外,synchronized 和 final 也可以保证可⻅性。同步块可⻅性由"对⼀个变量执⾏ unlock 前必须先把此变量同步回主内存,即先执⾏ store 和 write"这条规则获得。final 的可⻅性指:被 final 修饰的字段在构造⽅法中⼀旦初始化完成,并且构造⽅法没有把 this 引⽤传递出去,那么其他线程就能看到 final 字段的值。

有序性

有序性可以总结为:在本线程内观察所有操作是有序的,在⼀个线程内观察另⼀个线程,所有操作都是⽆序的。前半句指 as-if-serial 语义,后半句指指令重排序和⼯作内存与主内存延迟现象。
Java 提供 volatile 和 synchronized 保证有序性,volatile 本身就包含禁⽌指令重排序的语义,⽽synchronized 保证⼀个变量在同⼀时刻只允许⼀条线程对其进⾏ lock 操作,确保持有同⼀个锁的两个同步块只能串⾏进⼊。

Q7:谈⼀谈 volatile

volite两种特性

JMM 为 volatile 定义了⼀些特殊访问规则,当变量被定义为 volatile 后具备两种特性:

  • 保证变量对所有线程可⻅

当⼀条线程修改了变量值,新值对于其他线程来说是⽴即可以得知的。volatile 变量在各个线程的⼯作内存中不存在⼀致性问题,但 Java 的运算操作符并⾮原⼦操作,导致 volatile 变量运算在并发下仍不安全。

  • 禁⽌指令重排序优化

使⽤ volatile 变量进⾏写操作,汇编指令带有 lock 前缀,相当于⼀个内存屏障,后⾯的指令不能重排到内存屏障之前。
使⽤ lock 前缀引发两件事:① 将当前处理器缓存⾏的数据写回系统内存。②使其他处理器的缓存⽆效。相当于对缓存变量做了⼀次 store 和 write 操作,让 volatile 变量的修改对其他处理器⽴即可⻅。

静态变量 i 执⾏多线程 i++ 的不安全问题

⾃增语句由 4 条字节码指令构成的,依次为 getstatic、 iconst_1、 iadd、 putstatic,当把i 的值取到操作栈顶时,volatile 保证了 i 值在此刻正确,但在执⾏ iconst_1、 iadd 时,其他线程可能已经改变了 i 值,操作栈顶的值就变成了过期数据,所以把较⼩的 i 值同步回了主内存。

适⽤场景

① 运算结果并不依赖变量的当前值。② ⼀写多读,只有单⼀的线程修改变量值。

内存语义

  • 写⼀个 volatile 变量时,把该线程⼯作内存中的值刷新到主内存。
  • 读⼀个 volatile 变量时,把该线程⼯作内存值置为⽆效,从主内存读取。

指令重排序特点

第⼆个操作是 volatile 写,不管第⼀个操作是什么都不能重排序,确保写之前的操作不会被重排序到写之后。
第⼀个操作是 volatile 读,不管第⼆个操作是什么都不能重排序,确保读之后的操作不会被重排序到读之前。
第⼀个操作是 volatile 写,第⼆个操作是 volatile 读不能重排序。

JSR-133 增强 volatile 语义的原因

在旧的内存模型中,虽然不允许 volatile 变量间重排序,但允许 volatile 变量与普通变量重排序,可能导致内存不可⻅问题。JSR-133 严格限制编译器和处理器对 volatile 变量与普通变量的重排序,确保
volatile 的写-读和锁的释放-获取具有相同的内存语义。

Q8:final 可以保证可⻅性吗?

final 可以保证可⻅性,被 final 修饰的字段在构造⽅法中⼀旦被初始化完成,并且构造⽅法没有把 this引⽤传递出去,在其他线程中就能看⻅ final 字段值。
在旧的 JMM 中,⼀个严重缺陷是线程可能看到 final 值改变。⽐如⼀个线程看到⼀个 int 类型 final 值为 0,此时该值是未初始化前的零值,⼀段时间后该值被某线程初始化,再去读这个 final 值会发现值变为 1。

为修复该漏洞,JSR-133 为 final 域增加重排序规则:只要对象是正确构造的(被构造对象的引⽤在构造⽅法中没有逸出),那么不需要使⽤同步就可以保证任意线程都能看到这个 final 域初始化后的值。

写 final 域重排序规则

禁⽌把 final 域的写重排序到构造⽅法之外,编译器会在 final 域的写后,构造⽅法的 return 前,插⼊⼀个 Store Store 屏障。确保在对象引⽤为任意线程可⻅之前,对象的 final 域已经初始化过。

读 final 域重排序规则

在⼀个线程中,初次读对象引⽤和初次读该对象包含的 final域,JMM禁⽌处理器重排序这两个操作。编译器在读 final域操作的前⾯插⼊⼀个 LoadLoad屏障,确保在读⼀个对象的 final域前⼀定会先读包含这个 final域的对象引⽤。

Q8:谈⼀谈 synchronized

每个 Java 对象都有⼀个关联的 monitor,使⽤ synchronized 时 JVM 会根据使⽤环境找到对象的monitor,根据 monitor 的状态进⾏加解锁的判断。如果成功加锁就成为该 monitor 的唯⼀持有者,monitor 在被释放前不能再被其他线程获取。
同步代码块使⽤ monitorenter和 monitorexit这两个字节码指令获取和释放 monitor。这两个字节码指令都需要⼀个引⽤类型的参数指明要锁定和解锁的对象,对于同步普通⽅法,锁是当前实例对象;对于静态同步⽅法,锁是当前类的 Class对象;对于同步⽅法块,锁是 synchronized括号⾥的对象。
执⾏ monitorenter 指令时,⾸先尝试获取对象锁。如果这个对象没有被锁定,或当前线程已经持有锁,就把锁的计数器加 1,执⾏ monitorexit 指令时会将锁计数器减 1。⼀旦计数器为 0 锁随即就被释放。
例如有两个线程 A、B竞争 monitor,当 A竞争到锁时会将 monitor中的 owner设置为 A,把 B阻塞并放到等待资源的 ContentionList队列。ContentionList中的部分线程会进⼊ EntryList,EntryList中的线程会被指定为 OnDeck竞争候选者,如果获得了锁资源将进⼊ Owner状态,释放锁后进⼊!Owner 状态。被阻塞的线程会进⼊ WaitSet。
被 synchronized 修饰的同步块对⼀条线程来说是可重⼊的,并且同步块在持有锁的线程释放锁前会阻塞其他线程进⼊。从执⾏成本的⻆度看,持有锁是⼀个重量级的操作。Java 线程是映射到操作系统的内核线程上的,如果要阻塞或唤醒⼀条线程,需要操作系统帮忙完成,不可避免⽤户态到核⼼态的转换。

不公平的原因

所有收到锁请求的线程⾸先⾃旋,如果通过⾃旋也没有获取锁将被放⼊ ContentionList,该做法对于已经进⼊队列的线程不公平。
为了防⽌ ContentionList 尾部的元素被⼤量线程进⾏ CAS 访问影响性能,Owner 线程会在释放锁时将ContentionList 的部分线程移动到 EntryList 并指定某个线程为 OnDeck 线程,该⾏为叫做竞争切换, 牺牲了公平性但提⾼了性能。

Q9:锁优化有哪些策略?

JDK 6 对 synchronized 做了很多优化,引⼊了⾃适应⾃旋、锁消除、锁粗化、偏向锁和轻量级锁等提⾼锁的效率,锁⼀共有 4 个状态,级别从低到⾼依次是:⽆锁、偏向锁、轻量级锁和重量级锁,状态会随竞争情况升级。锁可以升级但不能降级,这种只能升级不能降级的锁策略是为了提⾼锁获得和释放的效率。

Q10:⾃旋锁是什么?

同步对性能最⼤的影响是阻塞,挂起和恢复线程的操作都需要转⼊内核态完成。许多应⽤上共享数据的锁定只会持续很短的时间,为了这段时间去挂起和恢复线程并不值得。如果机器有多个处理器核⼼,我们可以让后⾯请求锁的线程稍等⼀会,但不放弃处理器的执⾏时间,看看持有锁的线程是否很快会释放锁。为了让线程等待只需让线程执⾏⼀个忙循环,这项技术就是⾃旋锁。
⾃旋锁在 JDK1.4就已引⼊,默认关闭,在 JDK6中改为默认开启。⾃旋不能代替阻塞,虽然避免了线程切换开销,但要占⽤处理器时间,如果锁被占⽤的时间很短,⾃旋的效果就会⾮常好,反之只会⽩⽩消 耗处理器资源。如果⾃旋超过了限定的次数仍然没有成功获得锁,就应挂起线程,⾃旋默认限定次数是10。

Q11:什么是⾃适应⾃旋?

JDK6 对⾃旋锁进⾏了优化,⾃旋时间不再固定,⽽是由前⼀次的⾃旋时间及锁拥有者的状态决定。
如果在同⼀个锁上,⾃旋刚刚成功获得过锁且持有锁的线程正在运⾏,虚拟机会认为这次⾃旋也很可能成功,进⽽允许⾃旋持续更久。如果⾃旋很少成功,以后获取锁时将可能直接省略掉⾃旋,避免浪费处理器资源。
有了⾃适应⾃旋,随着程序运⾏时间的增⻓,虚拟机对程序锁的状况预测就会越来越精准。

Q12:锁消除是什么?

锁消除指即时编译器对检测到不可能存在共享数据竞争的锁进⾏消除。
主要判定依据来源于逃逸分析,如果判断⼀段代码中堆上的所有数据都只被⼀个线程访问,就可以当作栈上的数据对待,认为它们是线程私有的⽽⽆须同步。

Q13:锁粗化是什么?

原则需要将同步块的作⽤范围限制得尽量⼩,只在共享数据的实际作⽤域中进⾏同步,这是为了使等待锁的线程尽快拿到锁。
但如果⼀系列的连续操作都对同⼀个对象反复加锁和解锁,甚⾄加锁操作是出现在循环体之外的,即使没有线程竞争也会导致不必要的性能消耗。因此如果虚拟机探测到有⼀串零碎的操作都对同⼀个对象加锁,将会把同步的范围扩展到整个操作序列的外部。

Q14:偏向锁是什么?

偏向锁是为了在没有竞争的情况下减少锁开销,锁会偏向于第⼀个获得它的线程,如果在执⾏过程中锁⼀直没有被其他线程获取,则持有偏向锁的线程将不需要进⾏同步。
当锁对象第⼀次被线程获取时,虚拟机会将对象头中的偏向模式设为 1,同时使⽤ CAS 把获取到锁的线程 ID 记录在对象的 Mark Word 中。如果 CAS 成功,持有偏向锁的线程以后每次进⼊锁相关的同步块都不再进⾏任何同步操作。
⼀旦有其他线程尝试获取锁,偏向模式⽴即结束,根据锁对象是否处于锁定状态决定是否撤销偏向,后续同步按照轻量级锁那样执⾏。

Q15:轻量级锁是什么?

轻量级锁是为了在没有竞争的前提下减少重量级锁使⽤操作系统互斥量产⽣的性能消耗。
在代码即将进⼊同步块时,如果同步对象没有被锁定,虚拟机将在当前线程的栈帧中建⽴⼀个锁记录空间,存储锁对象⽬前 MarkWord的拷⻉。然后虚拟机使⽤ CAS尝试把对象的 MarkWord更新为指向锁记录的指针,如果更新成功即代表该线程拥有了锁,锁标志位将转变为 00,表示处于轻量级锁定状态。
如果更新失败就意味着⾄少存在⼀条线程与当前线程竞争。虚拟机检查对象的 MarkWord是否指向当前线程的栈帧,如果是则说明当前线程已经拥有了锁,直接进⼊同步块继续执⾏,否则说明锁对象已经被其他线程抢占。如果出现两条以上线程争⽤同⼀个锁,轻量级锁就不再有效,将膨胀为重量级锁,锁 标志状态变为 10,此时MarkWord存储的就是指向重量级锁的指针,后⾯等待锁的线程也必须阻塞。
解锁同样通过 CAS 进⾏,如果对象 Mark Word 仍然指向线程的锁记录,就⽤ CAS 把对象当前的 Mark Word 和线程复制的 Mark Word 替换回来。假如替换成功同步过程就顺利完成了,如果失败则说明有其他线程尝试过获取该锁,就要在释放锁的同时唤醒被挂起的线程。

Q16:偏向锁、轻量级锁和重量级锁的区别?

偏向锁的优点是加解锁不需要额外消耗,和执⾏⾮同步⽅法⽐仅存在纳秒级差距,缺点是如果存在锁竞争会带来额外锁撤销的消耗,适⽤只有⼀个线程访问同步代码块的场景。
轻量级锁的优点是竞争线程不阻塞,程序响应速度快,缺点是如果线程始终得不到锁会⾃旋消耗CPU,适⽤追求响应时间、同步代码块执⾏快的场景。
重量级锁的优点是线程竞争不使⽤⾃旋不消耗CPU,缺点是线程会阻塞,响应时间慢,适应追求吞吐量、同步代码块执⾏慢的场景。

Q17:Lock 和 synchronized 有什么区别?

Lock 接口是 juc 包的顶层接⼝,基于Lock 接⼝,⽤户能够以⾮块结构来实现互斥同步,摆脱了语⾔特性束缚,在类库层⾯实现同步。Lock 并未⽤到 synchronized,⽽是利⽤了 volatile 的可⻅性。
重⼊锁 ReentrantLock 是 Lock 最常⻅的实现,与 synchronized ⼀样可重⼊,不过它增加了⼀些⾼级功能:
**等待可中断: **持有锁的线程⻓期不释放锁时,正在等待的线程可以选择放弃等待⽽处理其他事情。
**公平锁:**公平锁指多个线程在等待同⼀个锁时,必须按照申请锁的顺序来依次获得锁,⽽⾮公平锁不保证这⼀点,在锁被释放时,任何线程都有机会获得锁。synchronized 是⾮公平的,ReentrantLock 在默认情况下是⾮公平的,可以通过构造⽅法指定公平锁。⼀旦使⽤了公平锁,性能会急剧下降,影响吞吐量。
**锁绑定多个条件: **⼀个 ReentrantLock 可以同时绑定多个 Condition。synchronized 中锁对象的wait跟 notify 可以实现⼀个隐含条件,如果要和多个条件关联就不得不额外添加锁,⽽ ReentrantLock 可以多次调⽤ newCondition 创建多个条件。

⼀般优先考虑使⽤ synchronized:① synchronized 是语法层⾯的同步,⾜够简单。② Lock 必须确保在 finally 中释放锁,否则⼀旦抛出异常有可能永远不会释放锁。使⽤ synchronized 可以由 JVM 来确保即使出现异常锁也能正常释放。③ 尽管 JDK5 时 ReentrantLock 的性能优于 synchronized,但在 JDK6 进⾏锁优化后⼆者的性能基本持平。从⻓远来看 JVM 更容易针对synchronized 优化,因为 JVM 可以在线程和对象的元数据中记录 synchronized 中锁的相关信息,⽽使⽤ Lock 的话 JVM 很难得知具体哪些锁对象是由特定线程持有的。

Q18:ReentrantLock 的可重⼊是怎么实现的?

以⾮公平锁为例,通过 ⽅法获取锁,该⽅法增加了再次获取同步状态的处理逻辑:判断当前线程是否为获取锁的线程来决定获取是否成功,如果是获取锁的线程再次请求则将同步状 态值增加并返回 true,表示获取同步状态成功。
成功获取锁的线程再次获取锁将增加同步状态值,释放同步状态时将减少同步状态值。如果锁被获取了
n次,那么前n-1次 ⽅法必须都返回fasle,只有同步状态完全释放才能返回true,该⽅
法将同步状态是否为 0 作为最终释放条件,释放时将占有线程设置为null 并返回 true。
对于⾮公平锁只要 CAS 设置同步状态成功则表示当前线程获取了锁,⽽公平锁则不同。公平锁使⽤
⽅法,该⽅法与 nonfairTryAcquire的唯⼀区别就是判断条件中多了对同步队列中当前节点是否有前驱节点的判断,如果该⽅法返回 true 表示有线程⽐当前线程更早请求锁,因此需要等待前驱线程获取并释放锁后才能获取锁。

Q19:什么是读写锁?

ReentrantLock 是排他锁,同⼀时刻只允许⼀个线程访问,读写锁在同⼀时刻允许多个读线程访问,在写线程访问时,所有的读写线程均阻塞。读写锁维护了⼀个读锁和⼀个写锁,通过分离读写锁使并发性 相⽐排他锁有了很⼤提升。
读写锁依赖 AQS 来实现同步功能,读写状态就是其同步器的同步状态。读写锁的⾃定义同步器需要在同步状态,即⼀个 int 变量上维护多个读线程和⼀个写线程的状态。读写锁将变量切分成了两个部分,
⾼ 16 位表示读,低 16 位表示写。

写锁是可重⼊排他锁,如果当前线程已经获得了写锁则增加写状态,如果当前线程在获取写锁时,读锁 已经被获取或者该线程不是已经获得写锁的线程则进⼊等待。写锁的释放与 ReentrantLock的释放类似,每次释放减少写状态,当写状态为 0时表示写锁已被释放。
读锁是可重⼊共享锁,能够被多个线程同时获取,在没有其他写线程访问时,读锁总会被成功获取。如 果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取则进
⼊等待。读锁每次释放会减少读状态,减少的值是(1<<16),读锁的释放是线程安全的。
锁降级指把持住当前拥有的写锁,再获取读锁,随后释放先前拥有的写锁。
锁降级中读锁的获取是必要的,这是为了保证数据可⻅性,如果当前线程不获取读锁⽽直接释放写锁, 假设此刻另⼀个线程 A获取写锁修改了数据,当前线程⽆法感知线程 A的数据更新。如果当前线程获取读锁,遵循锁降级的步骤,A将被阻塞,直到当前线程使⽤数据并释放读锁之后,线程 A才能获取写锁进⾏数据更新。

Q20:AQS 了解吗?

AQS队列同步器是⽤来构建锁或其他同步组件的基础框架,它使⽤⼀个 volatileintstate变量作为共享资源,如果线程获取资源失败,则进⼊同步队列等待;如果获取成功就执⾏临界区代码,释放资源时会 通知同步队列中的等待线程。
同步器的主要使⽤⽅式是继承,⼦类通过继承同步器并实现它的抽象⽅法来管理同步状态,对同步状态

进⾏更改需要使⽤同步器提供的 3个⽅法 getState、 setState 和
,它们保

证状态改变是安全的。⼦类推荐被定义为⾃定义同步组件的静态内部类,同步器⾃身没有实现任何同步 接⼝,它仅仅定义若⼲同步状态获取和释放的⽅法,同步器既⽀持独占式也⽀持共享式。
同步器是实现锁的关键,在锁的实现中聚合同步器,利⽤同步器实现锁的语义。锁⾯向使⽤者,定义了 使⽤者与锁交互的接⼝,隐藏实现细节;同步器⾯向锁的实现者,简化了锁的实现⽅式,屏蔽了同步状 态管理、线程排队、等待与唤醒等底层操作。
每当有新线程请求资源时都会进⼊⼀个等待队列,只有当持有锁的线程释放锁资源后该线程才能持有资 源。等待队列通过双向链表实现,线程被封装在链表的 Node节点中,Node的等待状态包括: CANCELLED(线程已取消)、SIGNAL(线程需要唤醒)、CONDITION(线程正在等待)、PROPAGATE(后继节点会传播唤醒操作,只在共享模式下起作⽤)。

Q21:AQS 有哪两种模式?

独占模式表示锁只会被⼀个线程占⽤,其他线程必须等到持有锁的线程释放锁后才能获取锁,同⼀时间 只能有⼀个线程获取到锁。
共享模式表示多个线程获取同⼀个锁有可能成功,ReadLock 就采⽤共享模式。
独占模式通过 acquire 和 release ⽅法获取和释放锁,共享模式通过 acquireShared 和 releaseShared
⽅法获取和释放锁。

Q22:AQS 独占式获取/释放锁的原理?

获取同步状态时,调⽤
⽅法,维护⼀个同步队列,使⽤
⽅法安全地获取线程

同步状态,获取失败的线程会被构造同步节点并通过 ⽅法加⼊到同步队列的尾部,在队列
中⾃旋。之后调⽤ ⽅法使得该节点以死循环的⽅式获取同步状态,如果获取不到则阻
塞,被阻塞线程的唤醒主要依靠前驱节点的出队或被中断实现,移出队列或停⽌⾃旋的条件是前驱节点 是头结点且成功获取了同步状态。

释放同步状态时,同步器调⽤
⽅法释放同步状态,然后调⽤
⽅法唤

醒头节点的后继节点,使后继节点重新尝试获取同步状态。

Q23:为什么只有前驱节点是头节点时才能尝试获取同步 状态?

头节点是成功获取到同步状态的节点,后继节点的线程被唤醒后需要检查⾃⼰的前驱节点是否是头节 点。
⽬的是维护同步队列的 FIFO 原则,节点和节点在循环检查的过程中基本不通信,⽽是简单判断⾃⼰的前驱是否为头节点,这样就使节点的释放规则符合 FIFO,并且也便于对过早通知的处理,过早通知指前驱节点不是头节点的线程由于中断被唤醒。

Q24:AQS 共享式式获取/释放锁的原理?

获取同步状态时,调⽤
⽅法,该⽅法调⽤
⽅法尝试获取同步状

态,返回值为 int 类型,返回值不⼩于于 0 表示能获取同步状态。因此在共享式获取锁的⾃旋过程中, 成功获取同步状态并退出⾃旋的条件就是该⽅法的返回值不⼩于0。

释放同步状态时,调⽤区别在于
⽅法,释放后会唤醒后续处于等待状态的节点。它和独占式的
⽅法必须确保同步状态安全释放,通过循环 CAS 保证,因为释放同步状

态的操作会同时来⾃多个线程。

Q25:线程的⽣命周期有哪些状态?

NEW:新建状态,线程被创建且未启动,此时还未调⽤
⽅法。

RUNNABLE:Java 将操作系统中的就绪和运⾏两种状态统称为 RUNNABLE,此时线程有可能在等待时间⽚,也有可能在执⾏。

BLOCKED:阻塞状态,可能由于锁被其他线程占⽤、调⽤了 等。
或 ⽅法、执⾏了wait⽅法

WAITING:等待状态,该状态线程不会被分配 CPU 时间⽚,需要其他线程通知或中断。可能由于调⽤
了⽆参的 和 ⽅法。
TIME_WAITING:限期等待状态,可以在指定时间内⾃⾏返回。导可能由于调⽤了带参的 和
⽅法。
TERMINATED:终⽌状态,表示当前线程已执⾏完毕或异常退出。

Q26:线程的创建⽅式有哪些?

① 继承 Thread 类并重写 run ⽅法。实现简单,但不符合⾥⽒替换原则,不可以继承其他类。
② 实现 Runnable 接⼝并重写 run ⽅法。避免了单继承局限性,编程更加灵活,实现解耦。
③实现 Callable 接⼝并重写 call ⽅法。可以获取线程执⾏结果的返回值,并且可以抛出异常。

Q27:线程有哪些⽅法?

① ⽅***导致当前线程进⼊休眠状态,与
不同的是该⽅法不会释放锁资源,进⼊的是

TIMED-WAITING 状态。
② ⽅法使当前线程让出 CPU时间⽚给优先级相同或更⾼的线程,回到 RUNNABLE状态,与其
他线程⼀起重新竞争CPU时间⽚。
③ ⽅法⽤于等待其他线程运⾏终⽌,如果当前线程调⽤了另⼀个线程的 join⽅法,则当前线程
进⼊阻塞状态,当另⼀个线程结束时当前线程才能从阻塞状态转为就绪态,等待获取CPU时间⽚。底层 使⽤的是wait,也会释放锁。

Q28:什么是守护线程?

守护线程是⼀种⽀持型线程,可以通过前设置。
将线程设置为守护线程,但必须在线程启动

守护线程被⽤于完成⽀持性⼯作,但在 JVM 退出时守护线程中的 finally 块不⼀定执⾏,因为 JVM 中没有⾮守护线程时需要⽴即退出,所有守护线程都将⽴即终⽌,不能靠在守护线程使⽤ finally 确保关闭资源。

Q29:线程通信的⽅式有哪些?

命令式编程中线程的通信机制有两种,共享内存和消息传递。在共享内存的并发模型⾥线程间共享程序 的公共状态,通过写-读内存中的公共状态进⾏隐式通信。在消息传递的并发模型⾥线程间没有公共状态,必须通过发送消息来显式通信。Java 并发采⽤共享内存模型,线程之间的通信总是隐式进⾏,整个通信过程对程序员完全透明。
**volatile **告知程序任何对变量的读需要从主内存中获取,写必须同步刷新回主内存,保证所有线程对变量访问的可⻅性。
**synchronized **确保多个线程在同⼀时刻只能有⼀个处于⽅法或同步块中,保证线程对变量访问的原⼦性、可⻅性和有序性。
等待通知机制指⼀个线程A调⽤了对象的 ⽅法进⼊等待状态,另⼀线程B调⽤了对象的
notify/notifyAll⽅法,线程A收到通知后结束阻塞并执⾏后序操作。对象上的 和
notify/notifyAll 如同开关信号,完成等待⽅和通知⽅的交互。

如果⼀个线程执⾏了某个线程的
⽅法,这个线程就会阻塞等待执⾏了
⽅法的线程终⽌,

这⾥涉及等待/通知机制。 join 底层通过通知所有等待在该线程对象上的线程。
实现,线程终⽌时会调⽤⾃身的
⽅法,

管道 IO 流⽤于线程间数据传输,媒介为内存。PipedOutputStream 和 PipedWriter 是输出流,相当于
⽣产者,PipedInputStream 和 PipedReader 是输⼊流,相当于消费者。管道流使⽤⼀个默认⼤⼩为
1KB 的循环缓冲数组。输⼊流从缓冲数组读数据,输出流往缓冲数组中写数据。当数组已满时,输出流所在线程阻塞;当数组⾸次为空时,输⼊流所在线程阻塞。
**ThreadLocal **是线程共享变量,但它可以为每个线程创建单独的副本,副本值是线程私有的,互相之间不影响。

Q30:线程池有什么好处?

降低资源消耗,复⽤已创建的线程,降低开销、控制最⼤并发数。
隔离线程环境,可以配置独⽴线程池,将较慢的线程与较快的隔离开,避免相互影响。 实现任务线程队列缓冲策略和拒绝机制。
实现某些与时间相关的功能,如定时执⾏、周期执⾏等。

Q31:线程池处理任务的流程?

① 核⼼线程池未满,创建⼀个新的线程执⾏任务,此时 workCount < corePoolSize。
② 如果核⼼线程池已满,⼯作队列未满,将线程存储在⼯作队列,此时 workCount >= corePoolSize。
③ 如果⼯作队列已满,线程数⼩于最⼤线程数就创建⼀个新线程处理任务,此时 workCount < maximumPoolSize,这⼀步也需要获取全局锁。
④ 如果超过⼤⼩线程数,按照拒绝策略来处理任务,此时 workCount > maximumPoolSize。
线程池创建线程时,会将线程封装成⼯作线程 Worker,Worker 在执⾏完任务后还会循环获取⼯作队列中的任务来执⾏。

Q32:有哪些创建线程池的⽅法?

可以通过 Executors 的静态⼯⼚⽅法创建线程池:
① newFixedThreadPool,固定⼤⼩的线程池,核⼼线程数也是最⼤线程数,不存在空闲线程,keep AliveTime = 0。该线程池使⽤的⼯作队列是⽆界阻塞队列 LinkedBlockingQueue,适⽤于负载较重的服务器。
② newSingleThreadExecutor,使⽤单线程,相当于单线程串⾏执⾏所有任务,适⽤于需要保证顺序执⾏任务的场景。

③ newCachedThreadPool,maximumPoolSize 设置为 Integer 最⼤值,是⾼度可伸缩的线程池。该线程池使⽤的⼯作队列是没有容量的 SynchronousQueue,如果主线程提交任务的速度⾼于线程处理的速度,线程池会不断创建新线程,极端情况下会创建过多线程⽽耗尽CPU 和内存资源。适⽤于执⾏很多短期异步任务的⼩程序或负载较轻的服务器。
④ newScheduledThreadPool:线程数最⼤为 Integer 最⼤值,存在 OOM ⻛险。⽀持定期及周期性任务执⾏,适⽤需要多个后台线程执⾏周期任务,同时需要限制线程数量的场景。相⽐ Timer 更安全,
功能更强,与 的区别是不回收⼯作线程。
⑤ newWorkStealingPool:JDK8 引⼊,创建持有⾜够线程的线程池⽀持给定的并⾏度,通过多个队列减少竞争。

Q33:创建线程池有哪些参数?

① corePoolSize:常驻核⼼线程数,如果为 0,当执⾏完任务没有任何请求时会消耗线程池;如果⼤于0,即使本地任务执⾏完,核⼼线程也不会被销毁。该值设置过⼤会浪费资源,过⼩会导致线程的频繁 创建与销毁。
② maximumPoolSize:线程池能够容纳同时执⾏的线程最⼤数,必须⼤于等于 1,如果与核⼼线程数设置相同代表固定⼤⼩线程池。
③ keep AliveTime:线程空闲时间,线程空闲时间达到该值后会被销毁,直到只剩下 corePoolSize 个线程为⽌,避免浪费内存资源。
④ unit:keep AliveTime 的时间单位。
⑤ workQueue:⼯作队列,当线程请求数⼤于等于 corePoolSize 时线程会进⼊阻塞队列。
⑥ threadFactory:线程⼯⼚,⽤来⽣产⼀组相同任务的线程。可以给线程命名,有利于分析错误。
⑦ handler:拒绝策略,默认使⽤ AbortPolicy 丢弃任务并抛出异常,CallerRunsPolicy 表示重新尝试提交该任务,DiscardOldestPolicy 表示抛弃队列⾥等待最久的任务并把当前任务加⼊队列, DiscardPolicy 表示直接抛弃当前任务但不抛出异常。

Q34:如何关闭线程池?

可 以 调 ⽤ 调⽤线程的
或 ⽅法关闭线程池,原理是遍历线程池中的⼯作线程,然后逐个
⽅法中断线程,⽆法响应中断的任务可能永远⽆法终⽌。

区别是
⾸先将线程池的状态设为 STOP,然后尝试停⽌正在执⾏或暂停任务的线程,并

返回等待执⾏任务的列表。⽽执⾏任务的线程。
只是将线程池的状态设为 SHUTDOWN,然后中断没有正在

通常调⽤ 来关闭线程池,如果任务不⼀定要执⾏完可调⽤shutdownNow。

Q35:线程池的选择策略有什么?

可以从以下⻆度分析:①任务性质:CPU 密集型、IO 密集型和混合型。②任务优先级。③任务执⾏时间。④任务依赖性:是否依赖其他资源,如数据库连接。
性质不同的任务可⽤不同规模的线程池处理,CPU 密集型任务应配置尽可能⼩的线程,如配置 Ncpu+1
个线程的线程池。由于 IO 密集型任务线程并不是⼀直在执⾏任务,应配置尽可能多的线程,如
2*Ncpu。混合型的任务,如果可以拆分,将其拆分为⼀个 CPU密集型任务和⼀个 IO密集型任务,只要两个任务执⾏的时间相差不⼤那么分解后的吞吐量将⾼于串⾏执⾏的吞吐量,如果相差太⼤则没必要 分解。
优先级不同的任务可以使⽤优先级队列 PriorityBlockingQueue 处理。
执⾏时间不同的任务可以交给不同规模的线程池处理,或者使⽤优先级队列让执⾏时间短的任务先执
⾏。
依赖数据库连接池的任务,由于线程提交 SQL 后需要等待数据库返回的结果,等待的时间越⻓ CPU 空闲的时间就越⻓,因此线程数应该尽可能地设置⼤⼀些,提⾼ CPU 的利⽤率。
建议使⽤有界队列,能增加系统的稳定性和预警能⼒,可以根据需要设置的稍微⼤⼀些。

Q36:阻塞队列有哪些选择?

阻塞队列⽀持阻塞插⼊和移除,当队列满时,阻塞插⼊元素的线程直到队列不满。当队列为空时,获取 元素的线程会被阻塞直到队列⾮空。阻塞队列常⽤于⽣产者和消费者的场景,阻塞队列就是⽣产者⽤来 存放元素,消费者⽤来获取元素的容器。

Java 中的阻塞队列

ArrayBlockingQueue,由数组组成的有界阻塞队列,默认情况下不保证线程公平,有可能先阻塞的线 程最后才访问队列。
LinkedBlockingQueue,由链表结构组成的有界阻塞队列,队列的默认和最⼤⻓度为 Integer 最⼤值。
PriorityBlockingQueue,⽀持优先级的⽆界阻塞队列,默认情况下元素按照升序排序。可⾃定义
⽅法指定排序规则,或者初始化时指定 Comparator 排序,不能保证同优先级元素的顺
序。
DelayQueue,⽀持延时获取元素的⽆界阻塞队列,使⽤优先级队列实现。创建元素时可以指定多久才 能从队列中获取当前元素,只有延迟期满时才能从队列中获取元素,适⽤于缓存和定时调度。
SynchronousQueue,不存储元素的阻塞队列,每⼀个 put 必须等待⼀个 take。默认使⽤⾮公平策略,也⽀持公平策略,适⽤于传递性场景,吞吐量⾼。
LinkedTransferQueue,链表组成的⽆界阻塞队列,相对于其他阻塞队列多了 和
⽅法。 transfer⽅法:如果当前有消费者正等待接收元素,可以把⽣产者传⼊的元素⽴刻传输给消费者,否则会将元素放在队列的尾节点并等到该元素被消费者消费才返回。 tryTransfer ⽅法⽤来试探⽣产者传⼊的元素能否直接传给消费者,如果没有消费者等待接收元素则返回 false,和
的区别是⽆论消费者是否消费都会⽴即返回。
LinkedBlockingDeque,链表组成的双向阻塞队列,可从队列的两端插⼊和移出元素,多线程同时⼊队 时减少了竞争。

实现原理

使⽤通知模式实现,⽣产者往满的队列⾥添加元素时会阻塞,当消费者消费后,会通知⽣产者当前队列 可⽤。当往队列⾥插⼊⼀个元素,如果队列不可⽤,阻塞⽣产者主要通过 LockSupport的 park⽅法

实现,不同操作系统中实现⽅式不同,在 Linux 下使⽤的是系统⽅法
实现。

Q37:谈⼀谈 ThreadLocal

ThreadLoacl是线程共享变量,主要⽤于⼀个线程内跨类、⽅法传递数据。ThreadLoacl有⼀个静态内部类 ThreadLocalMap,其 Key是 ThreadLocal对象,值是 Entry对象,Entry中只有⼀个 Object类的 vaule值。ThreadLocal是线程共享的,但 ThreadLocalMap是每个线程私有的。ThreadLocal主要有 set、get和 remove三个⽅法。

set ⽅法

⾸先获取当前线程,然后再获取当前线程对应的 ThreadLocalMap 类型的对象 map。如果 map 存在就直接设置值,key 是当前的 ThreadLocal 对象,value 是传⼊的参数。

如果 map 不存在就通过

get ⽅法

⽅法为当前线程创建⼀个 ThreadLocalMap 对象再设置值。

⾸先获取当前线程,然后再获取当前线程对应的 ThreadLocalMap 类型的对象 map。如果 map 存在就以当前 ThreadLocal 对象作为 key 获取 Entry 类型的对象 e,如果 e 存在就返回它的 value 属性。

如果 e 不存在或者 map 不存在,就调⽤
ThreadLocalMap 对象然后返回默认的初始值 null。

remove ⽅法

⽅法先为当前线程创建⼀个

⾸先通过当前线程获取其对应的 ThreadLocalMap 类型的对象 m,如果 m 不为空,就解除
ThreadLocal 这个 key 及其对应的 value 值的联系。

存在的问题

线程复⽤会产⽣脏数据,由于线程池会重⽤ Thread对象,因此与 Thread绑定的 ThreadLocal也会被重⽤。如果没有调⽤ remove清理与线程相关的 ThreadLocal信息,那么假如下⼀个线程没有调⽤ set 设置初始值就可能 get到重⽤的线程信息。
ThreadLocal 还存在内存泄漏的问题,由于 ThreadLocal 是弱引⽤,但 Entry 的 value 是强引⽤,因此当 ThreadLocal 被垃圾回收后,value 依旧不会被释放。因此需要及时调⽤ remove ⽅法进⾏清理操作。

Q38:什么是死锁?

Dead lock is a bloking phenomenon when two threads each hold an exclusive lock that the other thread needs. If there is no force invoked, the program is not able to go forward.

死锁,简单来说,就是两个或者两个以上的线程在执行的过程中,去争夺同样一个共享资源而造成的互相等待的一个现象。如果没有外部干预,线程会一直阻塞下去,无法往下执行。这样一直处于互相等待资源的线程,我们称为死锁线程。
导致死锁有4个必要的条件,当这个4个条件被同时满足,死锁就会发生。这4个条件分别是:

  • 互斥条件,共享资源 X 和 Y 只能被一个线程占用。
  • 请求和保持条件,线程 T1 已经取得共享资源 X , 在等待共享资源 Y 的时候,不释放共享资源 X
  • 不可抢占条件,其他线程不能强行抢占线程 T1 占有的资源
  • 循环等待条件,线程 T1 等待线程 T2 占有的资源,线程 T2 等待线程 T1 占有的资源就是循环等待

导致死锁以后,只能通过人工干预来解决,比如说重启服务或者kill掉这个进程。所以说,我们只能在写代码的时候去规避可能出现的死锁问题。而按照死锁发生的4个条件,我们只需要去破坏其中任何一个便可以去解决它。但是互斥条件是没有办法破坏的,因为它是互斥锁的基本约束,而其他的三个条件我们都有办法来破坏。

  • 对于请求和保持条件,我们可以一次性申请所有的资源,这样就不存在锁要等待了。
  • 对于不可抢占条件,占用部分资源的线程在进一步申请其他资源的时候,如果申请不到,可以主动去释放占用资源。
  • 对于循环等待条件,可以按序申请资源来预防。所谓按序申请,也就是资源是有线性顺序的,申请的时候,可以先申请资源序号小的,然后再去申请资源序号大的,这样线性化之后呢,自然就不存在循环了。

以上就是我对这个问题的理解。

2022最新的高并发多线程面试题,一文掌握offer钥匙相关推荐

  1. 推测的删除锁(Speculative Lock Elision):实现高并发多线程执行

    背景 SLE全称Speculative Lock Elision,我称之为推测的删除锁.这是一篇关于SLE的论文翻译,但是因为本人英语功底很差,所以翻译的不通顺而且会有很多错误的地方.之所以把它发出来 ...

  2. 高并发多线程分片断点下载

    基于Java的高并发多线程分片断点下载 首先直接看测试情况: 单线程下载72MB文件 7线程并发分片下载72MB文件: 下载效率提高2-3倍,当然以上测试结果还和设备CPU核心数.网络带宽息息相关. ...

  3. 面试高薪程序员之高频面试题(一),集合,JVM,高并发多线程等

    一,java集合类 Java集合比如说HashMap和ConcurrentHashMap,HashMap底层实现原理?HashMap加载因子为什么是0.75?HashMap扩容操作可能会出现的问题?H ...

  4. JAVA高并发多线程必须懂的50个问题

    http://www.importnew.com/12773.html ImportNew 首页所有文章资讯Web架构基础技术书籍教程Java小组工具资源 Java线程面试题 Top 50 2014/ ...

  5. 多线程与高并发 笔记,非面向初学者 二:java引用,高并发多线程容器,线程池

    网页右边,向下滑有目录索引,可以根据标题跳转到你想看的内容 如果右边没有就找找左边 上一节:JUC锁,一些面试题和源码讲解 1.引用 java引用共4种,强软弱虚 强引用:我们普通的new一个对象,就 ...

  6. java 多进程写一个文件_java高并发多线程及多进程同时写入文件研究

    测试&思考: 环境:windows 七.linux centos 6.三.java8html java多线程同时写一个文件 java高并发环境下多线程同时写入一个文件时, 经过 FileLoc ...

  7. Java高并发多线程

    并发和多线程(线程池.SYNC和Lock锁机制.线程通信.volatile.ThreadLocal.CyclicBarrier.Atom包.CountDownLatch.AQS.CAS原理等等) 1. ...

  8. java高并发多线程及多进程同时写入文件研究

    文章目录 测试&思考: java多线程同时写一个文件 第一种情况是:一个线程A有对文件加锁,另一个线程B没对文件加锁 在windows7环境下:(持有锁的可以写文件成功). 在linux ce ...

  9. ArrayList问题之高并发多线程环境下会出现内部成员会出现null

    问题描述 最近在做飞机大战游戏,发现一个问题,就是游戏运行了一定时间后会停止,同时eclipse会报出空指针异常. 为此,我针对性地在 遍历list列表并取出列表元素 的for循环中输出每个对象,发现 ...

最新文章

  1. 福建省2013高职单招计算机类试题,13年福建-高职单招-计算机类试题及答案.doc
  2. 超年轻!93年小伙已是985大学教授、博导!
  3. Spring学习五(JDBC支持)
  4. php+ tinymce粘贴word
  5. I am the load of my word
  6. linux+内核中开启nfs,NFS Client in Linux Kernel - Open
  7. MySQL--更新自增列的潜在风险
  8. 2021年上半年内容型社交电商行业分析报告
  9. JAVA中的按值传递
  10. linux exec操作文件描述符
  11. SQL语句基本用法格式
  12. Linux之Shell编程详解
  13. 游戏一般用什么编程语言开发?
  14. 用Python实现黑客帝国代码雨效果
  15. win10计算机管理员权限删除,win10需要管理员权限删除文件怎么办?获取管理员权限删除文件夹...
  16. 使用Java的JNI调用C
  17. git的使用、ssh生成、github、Git分支操作
  18. 中国通信简史 (下)
  19. (程序员/软件工程师/开发者)编程——计算机专业英语学习指引
  20. 关于人工智能不会使大脑变懒惰的议论文_人工智能的好处和风险:所有您需要知道的...

热门文章

  1. 将Matlab绘图过程保存为视频
  2. chatgpt赋能python:Python两张图片对比:初学者与专家的编程经验
  3. 后端学习攻略,助你打怪升级
  4. Excel中常见数学函数使用
  5. spring Bean装配的几种方式简单介绍
  6. 微信小程序实现单选框以及复选框默认样式修改(超详细)
  7. STM32F10x互补输出TIM_OutputState,TIM_OutputNState的意思
  8. 华为----1-划分vlan
  9. 重学Java(四):操作符
  10. 全面HTML5化:火狐移动操作系统B2G价值几何