2023Java高频必背并发编程面试题02

15 篇文章 0 订阅
12 篇文章 0 订阅

1、ABA问题及解决⽅法简述。

CAS 算法是基于值来做⽐较的,如果当前有两个线程,⼀个线程将变量值从 A 改为 B ,再由 B 改回为 A,当前线程开始执⾏ CAS 算法时,就很容易认为值没有变化,误认为读取数据到执⾏ CAS 算法的期间,没有线程修改过数据。
juc 包提供了⼀个 AtomicStampedReference,即在原始的版本下加⼊版本号戳,解决 ABA 问题。

2、简述常⻅的Atomic类。

在很多时候,我们需要的仅仅是⼀个简单的、⾼效的、线程安全的++或者–⽅案,使⽤synchronized关键字和lock固然可以实现,但代价⽐较⼤,此时⽤原⼦类更加⽅便。
基本数据类型的原⼦类有
(1)AtomicInteger 原⼦更新整形;
(2)AtomicLong 原⼦更新⻓整型;
(3)AtomicBoolean 原⼦更新布尔类型。
Atomic数组类型有
(1)AtomicIntegerArray 原⼦更新整形数组⾥的元素;
(2)AtomicLongArray 原⼦更新⻓整型数组⾥的元素;
(3)AtomicReferenceArray 原⼦更新引⽤类型数组⾥的元素。
Atomic引⽤类型有
(1)AtomicReference 原⼦更新引⽤类型;
(2)AtomicMarkableReference 原⼦更新带有标记位的引⽤类型,可以绑定⼀个 boolean 标记;
(3)AtomicStampedReference 原⼦更新带有版本号的引⽤类型。
FieldUpdater类型
(1)AtomicIntegerFieldUpdater 原⼦更新整形字段的更新器;
(2)AtomicLongFieldUpdater 原⼦更新⻓整形字段的更新器;
(3)AtomicReferenceFieldUpdater 原⼦更新引⽤类型字段的更新器。

3、简述Atomic类基本实现原理。

以AtomicIntger 为例。⽅法getAndIncrement,以原⼦⽅式将当前的值加1,具体实现为:
(1)在 for 死循环中取得 AtomicInteger ⾥存储的数值
(2)对 AtomicInteger 当前的值加 1
(3)调⽤ compareAndSet ⽅法进⾏原⼦更新
(4)先检查当前数值是否等于 expect
(5)如果等于则说明当前值没有被其他线程修改,则将值更新为 next,
(6)如果不是会更新失败返回 false,程序会进⼊ for 循环重新进⾏ compareAndSet 操作。

4、简述CountDownLatch。

CountDownLatch这个类使⼀个线程等待其他线程各⾃执⾏完毕后再执⾏。是通过⼀个计数器来实现的,计数器的初始值是线程的数量。每当⼀个线程执⾏完毕后,调⽤countDown⽅法,计数器的值就减1,当计数器的值为0时,表示所有线程都执⾏完毕,然后在等待的线程就可以恢复⼯作了。只能⼀次性使⽤,不能reset。

5、简述CyclicBarrier。

CyclicBarrier 主要功能和CountDownLatch类似,也是通过⼀个计数器,使⼀个线程等待其他线程各⾃执⾏完毕后再执⾏。但是其可以重复使⽤(reset)。

6、简述Semaphore。

Semaphore即信号量。Semaphore 的构造⽅法参数接收⼀个 int 值,设置⼀个计数器,表示可⽤的许可数量即最⼤并发数。使⽤ acquire ⽅法获得⼀个许可证,计数器减⼀,使⽤ release ⽅法归还许可,计数器加⼀。如果此时计数器值为0,线程进⼊休眠。

7、简述Exchanger。

Exchanger类可⽤于两个线程之间交换信息。可简单地将Exchanger对象理解为⼀个包含两个格⼦的容器,通过exchanger⽅法可以向两个格⼦中填充信息。线程通过exchange ⽅法交换数据,第⼀个线程执⾏exchange ⽅法后会阻塞等待第⼆个线程执⾏该⽅法。当两个线程都到达同步点时这两个线程就可以交换数据当两个格⼦中的均被填充时,该对象会⾃动将两个格⼦的信息交换,然后返回给线程,从⽽实现两个线程的信息交换。

8、简述ConcurrentHashMap。

JDK7采⽤锁分段技术。⾸先将数据分成 Segment 数据段,然后给每⼀个数据段配⼀把锁,当⼀个线程占⽤锁访问其中⼀个段的数据时,其他段的数据也能被其他线程访问。
get 除读到空值不需要加锁。该⽅法先经过⼀次再散列,再⽤这个散列值通过散列运算定位到 Segment,最后通过散列算法定位到元素。put 须加锁,⾸先定位到 Segment,然后进⾏插⼊操作,第⼀步判断是否需要对 Segment ⾥的 HashEntry 数组进⾏扩容,第⼆步定位添加元素的位置,然后将其放⼊数组。
JDK8的改进
(1)取消分段锁机制,采⽤CAS算法进⾏值的设置,如果CAS失败再使⽤ synchronized 加锁添加元素;
(2)引⼊红⿊树结构,当某个槽内的元素个数超过8且 Node数组 容量⼤于 64 时,链表转为红⿊树;
(3)使⽤了更加优化的⽅式统计集合内的元素数量。

9、synchronized底层实现原理。

Java 对象底层都会关联⼀个 monitor,使⽤ synchronized 时 JVM 会根据使⽤环境找到对象的 monitor,根据 monitor 的状态进⾏加解锁的判断。如果成功加锁就成为该 monitor 的唯⼀持有者,monitor 在被释放前不能再被其他线程获取。
synchronized在JVM编译后会产⽣monitorenter 和 monitorexit 这两个字节码指令,获取和释放 monitor。
这两个字节码指令都需要⼀个引⽤类型的参数指明要锁定和解锁的对象,对于同步普通⽅法,锁是当前实例对象;对于静态同步⽅法,锁是当前类的 Class 对象;对于同步⽅法块,锁是synchronized 括号⾥的对象。
执⾏ monitorenter 指令时,⾸先尝试获取对象锁。如果这个对象没有被锁定,或当前线程已经持有锁,就把锁的计数器加 1,执⾏ monitorexit 指令时会将锁计数器减 1。⼀旦计数器为 0 锁随即就被释放。

10、synchronized关键词使⽤⽅法。

(1)直接修饰某个实例⽅法;
(2)直接修饰某个静态⽅法;
(3)修饰代码块。

11、简述Java偏向锁。

JDK 1.6 中提出了偏向锁的概念。该锁提出的原因是,开发者发现多数情况下锁并不存在竞争,⼀把锁往往是由同⼀个线程获得的。偏向锁并不会主动释放,这样每次偏向锁进⼊的时候都会判断该资源是否是偏向⾃⼰的,如果是偏向⾃⼰的则不需要进⾏额外的操作,直接可以进⼊同步操作。
其申请流程为
(1)⾸先需要判断对象的 Mark Word 是否属于偏向模式,如果不属于,那就进⼊轻量级锁判断逻辑。否则继续下⼀步判断;
(2)判断⽬前请求锁的线程 ID 是否和偏向锁本身记录的线程 ID ⼀致。如果⼀致,继续下⼀步的判断,如果不⼀致,跳转到步骤4;
(3)判断是否需要重偏向。如果不⽤的话,直接获得偏向锁;
(4)利⽤ CAS 算法将对象的 Mark Word 进⾏更改,使线程 ID 部分换成本线程 ID。如果更换成功,则重偏向完成,获得偏向锁。如果失败,则说明有多线程竞争,升级为轻量级锁。

12、简述轻量级锁。

轻量级锁是为了在没有竞争的前提下减少重量级锁出现并导致的性能消耗
其申请流程为:
(1)如果同步对象没有被锁定,虚拟机将在当前线程的栈帧中建⽴⼀个锁记录空间,存储锁对象⽬前 MarkWord 的拷⻉;
(2)虚拟机使⽤ CAS 尝试把对象的 Mark Word 更新为指向锁记录的指针;
(3)如果更新成功即代表该线程拥有了锁,锁标志位将转变为 00,表示处于轻量级锁定状态;
(4)如果更新失败就意味着⾄少存在⼀条线程与当前线程竞争。虚拟机检查对象的 Mark Word 是否指向当前线程的栈帧;
(5)如果指向当前线程的栈帧,说明当前线程已经拥有了锁,直接进⼊同步块继续执⾏;
(6)如果不是则说明锁对象已经被其他线程抢占;
(7)如果出现两条以上线程争⽤同⼀个锁,轻量级锁就不再有效,将膨胀为重量级锁,锁标志状态变为10,此时Mark Word 存储的就是指向重量级锁的指针,后⾯等待锁的线程也必须阻塞。

13、简述锁优化策略。

即⾃适应⾃旋、锁消除、锁粗化、锁升级等策略。

14、简述Java的⾃旋锁。

线程获取锁失败后,可以采⽤这样的策略,可以不放弃 CPU ,不停的重试内重试,这种操作也称为⾃旋锁。

15、简述⾃适应⾃旋锁。

⾃适应⾃旋锁⾃旋次数不再⼈为设定,通常由前⼀次在同⼀个锁上的⾃旋时间及锁的拥有者的状态决定。

16、简述锁粗化。

锁粗化的思想就是扩⼤加锁范围,避免反复的加锁和解锁。

17、简述锁消除。

锁消除是⼀种更为彻底的优化,在编译时,Java编译器对运⾏上下⽂进⾏扫描,去除不可能存在共享资源竞争的锁。

18、简述Lock与ReentrantLock。

Lock接⼝是 Java并发包的顶层接⼝。
可重⼊锁 ReentrantLock 是 Lock 最常⻅的实现,与 synchronized ⼀样可重⼊。ReentrantLock 在默认情况下是⾮公平的,可以通过构造⽅法指定公平锁。⼀旦使⽤了公平锁,性能会下降。

19、简述AQS。

AQS(AbstractQuenedSynchronizer)抽象的队列式同步器。AQS是将每⼀条请求共享资源的线程封装成⼀个锁队列的⼀个结点(Node),来实现锁的分配。AQS是⽤来构建锁或其他同步组件的基础框架,它使⽤⼀个 volatile int state 变量作为共享资源,如果线程获取资源失败,则进⼊同步队列等待;如果获取成功就执⾏临界区代码,释放资源时会通知同步队列中的等待线程。
⼦类通过继承同步器并实现它的抽象⽅法getState、setState 和 compareAndSetState对同步状态进⾏更改。
AQS获取独占锁/释放独占锁原理
(1)获取(acquire)
调⽤ tryAcquire ⽅法安全地获取线程同步状态,获取失败的线程会被构造同步节点并通过 addWaiter⽅法加⼊到同步队列的尾部,在队列中⾃旋;
调⽤ acquireQueued ⽅法使得该节点以死循环的⽅式获取同步状态,如果获取不到则阻塞。
(2)释放(release)
调⽤ tryRelease ⽅法释放同步状态;
调⽤ unparkSuccessor ⽅法唤醒头节点的后继节点,使后继节点重新尝试获取同步状态。
AQS获取共享锁/释放共享锁原理:
获取锁(acquireShared)
调⽤ tryAcquireShared ⽅法尝试获取同步状态,返回值不⼩于 0 表示能获取同步状态。
释放(releaseShared),并唤醒后续处于等待状态的节点。

20、死锁与活锁的区别,死锁与饥饿的区别?

死锁:是指两个或两个以上的进程(或线程)在执行过程中,因争夺资源而造成
的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
产生死锁的必要条件
(1)互斥条件:所谓互斥就是进程在某一时间内独占资源。
(2)请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
(3)不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
(4)循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
活锁:任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,
失败,尝试,失败。
活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的“活”, 而
处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。
饥饿:一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执
行的状态。
Java 中导致饥饿的原因:
(1)高优先级线程吞噬所有的低优先级线程的 CPU 时间。
(2)线程被永久堵塞在一个等待进入同步块的状态,因为其他线程总是能在它之前
持续地对该同步块进行访问。
(3)线程在等待一个本身也处于永久等待完成的对象(比如调用这个对象的 wait 方
法),因为其他线程总是被持续地获得唤醒。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Z_凌云

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值