线程中并发的安全面试题
线程中并发的安全(重点)
1.synchronized关键字的底层原理
难5出现3
基本回顾定义
不加锁会出现超卖的情况,但是会影响性能。
sunchronized
是对象锁采用互斥的方式让同一时间最多只有一个线程能持有对象锁,其他线程想获取这个对象锁时就会阻塞住
底层就是一个Monitor
运用反编译(javap)可以看见monitorenter
上锁moniterexit
解锁中间锁住的部分就是锁逻辑,会有两个moniterexit为什么会有两个呢 因为会隐式有一个tryfinally防止代码抛出异常,如果有异常就走下面的释放逻辑
Monitor
监视器,由jvm提供,由c++实现
Monitor
是三个属性WaitSet 、EntryList、Owner
。lock对象和Monitor
关联判断Owner是不是空,如果是就获得对象锁,再来一个线程的话就会看Owner是不是空如果不是空就进入List中等待,等待的就处于阻塞状态,等线程1执行完之后就争抢Owner获取锁。当一个方法使用了wait方法之后就会处于waitSet中
回答
- 1.
Synchronized
【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】 - 2.它的底层由
monitor
实现的,monitor是jvm级别的对象(C++实现)线程获得锁需要使用对象(锁)关联monitor - 3.在monitor内部有三个属性,分别是owner、entrylist、waitset其中owner是关联的获得锁的线程,并且只能关联一个线程;entrylist关联的是处于阻塞状态的线程;waitset关联的是处于Waiting状态的线程
2.Synchronized底层实现进阶
Monitor是重量级锁,里面涉及了用户态到内核态的切换,进程切换成本高,性能比较低。
jdk1.6之后引入了偏向锁和轻量级锁,他们的引入就是为了解决在没有多线程竞争或基本没有竞争的场景下使用传统锁所带来的内存消耗问题和性能开销的问题
重量级锁每一个java对象都可以关联一个Monitor对象,如果使用Synchronized东西上锁(重量级锁),该对象头的MarkWord就被设置指向Monitor对象的指针
在HotSpot虚拟机中,对象在内存中可以分成3块区域,对象头、实例数据、对象填充
MarkWord重点 看原视频吧 有点迷糊了
轻量级锁
在很多情况下,java程序在运行的时候都是不存在竞争的,意思就是不同的线程交替执行同步代码块里面的代码。这种情况下就可以使用轻量级锁,轻量级锁时可以重入的
偏向锁
只有第一次使用cas讲线程id设置到对象的Markword头,之后发现线程id是自己就表示没有竞争,不用重新cas以后只要不发生竞争这个对象就归线程所以
java中的Synchronized
有偏向锁,轻量级锁,重量级锁三种形式。分别对应了锁只被一个线程持有,不同线程交替持有锁,多线程竞争锁三种情况,一旦所发生竞争都会升级成重量级锁
总结
- 基本作用与底层基础
核心功能:作为对象锁,通过互斥机制保证同一时间最多只有一个线程持有锁,阻止多线程同时执行临界区代码,从而避免超卖等并发安全问题(但会带来一定性能开销)。 - 底层依赖:基于 JVM 提供的 Monitor(监视器,C++ 实现) 实现。线程获取锁的过程本质是竞争 Monitor 的所有权。
- Monitor 结构:包含 Owner(持有锁的线程,唯一)、EntryList(等待获取锁的阻塞线程)、WaitSet(调用 wait() 后等待被唤醒的线程)。线程竞争锁时,未获取到锁的线程进入 EntryList 阻塞;持有锁的线程调用 wait() 会释放锁并进入 WaitSet。
- 锁的升级机制(JDK 1.6 优化)
为解决传统重量级锁的性能问题(用户态与内核态切换开销大),引入了 锁升级策略(不可逆),适应不同竞争强度场景:
偏向锁:适用于单线程重复获取锁的场景。首次获取锁时,通过 CAS 将线程 ID 写入对象头的 MarkWord;后续该线程再次获取锁时,只需判断线程 ID 匹配即可,无需 CAS 操作,开销极小。
轻量级锁:适用于多线程交替执行同步代码的场景(轻度竞争)。线程获取锁时,会在栈帧中创建 Lock Record,并通过 CAS 将 MarkWord 指向该记录;若 CAS 成功则获取锁,失败则自旋重试(避免阻塞)。
重量级锁:适用于多线程激烈竞争的场景。当自旋失败(竞争加剧),轻量级锁升级为重量级锁,此时依赖 Monitor 和操作系统互斥量实现,未获取锁的线程进入 EntryList 阻塞(开销较大)。
总结
synchronized 通过 对象锁 + Monitor 机制 保证并发安全,同时通过 锁升级(偏向锁→轻量级锁→重量级锁) 优化性能,根据竞争强度动态切换锁的实现方式,平衡了线程安全与执行效率。
面试回答
关于synchronized的底层原理,我可以从核心作用、实现基础和优化机制三方面总结:
首先,它的核心是通过对象锁的互斥性保证并发安全:同一时间最多只有一个线程能持有锁,其他线程会阻塞,以此避免超卖等问题。
其次,底层依赖 JVM 的Monitor(监视器) 实现,这是一个 C++ 实现的 JVM 对象。同步代码块通过monitorenter和monitorexit指令控制锁的获取与释放(两个monitorexit确保异常时也能释放);同步方法则通过方法元数据的ACC_SYNCHRONIZED标志实现。Monitor 内部有Owner(持有锁的线程)、EntryList(阻塞等待锁的线程)、WaitSet(调用wait()后等待唤醒的线程)三个关键部分,负责线程的竞争与状态管理。
最后,JDK 1.6 引入了锁升级机制优化性能(升级不可逆):单线程场景用偏向锁,通过 MarkWord 记录线程 ID,避免重复 CAS;多线程交替执行时升级为轻量级锁,用栈帧中的 Lock Record 和 CAS 操作实现,减少阻塞;竞争激烈时升级为重量级锁,依赖操作系统互斥量,此时线程会进入阻塞状态,虽然开销大但能保证线程绝对安全。
简单说,synchronized通过 Monitor 实现基础同步,再结合锁升级策略,在安全与性能间做了很好的平衡。
3.jMM(java内存模型)
难3出现3
JMM(java Memory Model)内存模型,定义了共享内存中多线程程序读写的行为规范,通过这些规则来规范对内存读写操作从而保证指令的正确性。
工作内存中数据是对每个线程私有的,线程之间不可以互相访问,多个线程之间要同步数据只能通过主内存来同步数据
面试回答:
- JMM(java Memory Model)内存模型,定义了共享内存中多线程程序读写的行为规范,通过这些规则来规范对内存读写操作从而保证指令的正确性。
- jMM把内存分成了2块,一快十私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存)
- 线程和线程之前互相隔离,线程和线程之间交互需要通过主内存来进行
4.知道CAS吗
难3出现2
底层框架用的多 并发包很多包的类都用到了CAS 自旋锁
CAS的全称是Compare And Awap(比较再交换)体现乐观锁的思维在无锁的情况下保证线程操作共享的原子性。
自旋锁,没有枷锁,不会陷入阻塞状态,效率高
如果竞争激烈重试的频率高,效率就会收到影响就可以添加阈值,到多少之后还没有自旋成功就放弃
CAS底层是直接调用Unasfe类来直接调用操作系统底层的CAS指令
乐观锁和悲观锁
CAS是基于乐观锁的思想:最乐观的估计,不怕别的线程来修改共享变量,就算改了也没关系,我自旋在重试
Synchronized是基于悲观锁的思想实现的,最悲观的估计,放着其他线程来修改共享变量,我上了锁你们都别想改,我改完了之后你们才有机会
面试回答
CAS知道吗
全称就是Compare And Swap(比较再交换)体现一种乐观锁的思想,在没有锁状态下保证线程操作的原子性,再很多的框架中都使用了CAS,在操作共享变量的时候使用自旋锁,效率会更高一些,CAS的底层是调用Unsafe方法,是其他的语言实现的
乐观锁和悲观锁的区别
CAS是基于乐观锁实现的,最乐观的估计,不怕别的线程修改更新变量,就算改了也没事,重试呗
Syschronized是基于悲观锁实现的,最悲观的估计,防着其他线程来修改共享的变量,我上了锁你们都别想修改,我改完了解开锁你才有机会
5.谈一谈对Volatile的理解
难3出现3
关键字可以修饰共享的变量被volation修改之后就会有两层含义
1.保证多线程之间的可见性
用volatile修饰的变量,让一个线程对共享变量的修改可以对另一个线程可见,防止jit做优化(即使编译器会对修饰的变量做优化)
2.禁止指令的重新排序
用Volation修饰共享变量会在读,写。共享变量的时候加入不同的屏障,防止其他线程越过屏障,从而达到阻止重排序的效果
6.什么是AQS
难3出现3
AbstactQueuedSynchronizer抽象的队列同步器,构建锁和其他同步组件的基础
AQS与Synchronizer的区别
Synchronizer:关键字 c++实现 悲观锁,自动释放锁,重量级锁性能差
AQS java实现 悲观锁,手动开启和关闭 锁竞争激烈的情况下,提供了多种解决方案
基本工作原理 会维护一个Volital修饰的变量State多线程共享有0无锁1有锁两种情况,还会有个队列判断state为1就加入这个队列,队列是先进先出的队列回维护对首和对尾两个指针
AQS是公平锁还是非公平锁
都有吧,新线程与队列中的线程共同抢占资源是非公平锁。而对于队列中的线程永远都是让head线程获取锁是公平锁
面试回答
7.ReentrantLock实现原理
难4出现3
reentrantLock可重入锁
相对于synchronized有好处:可中断 可设置过期时间 可设置公平锁 支持多个条件变量
主要利用了CAS+AQS来实现,支持公平锁和非公平锁,有两个构造方法一个有参一个无参构造true是公平锁,flase是非公平锁,公平锁吐出量低,会继承AQS(AbstartQueuedSysnchronizer)
- 线程来抢锁后使用cas的方式修改state状态,修改状态成功为1,则让exclusiveOwnerThread属性指向当前线程,获取锁成功
- 假如修改状态失败,则会进入双向队列中等待,head指向双向队列头部,tail指向双向队列尾部
- 当exclusiveOwnerThread为null的时候,则会唤醒在双向队列中等待的线程
- 公平锁则体现在按照先后顺序获取锁,非公平体现在不在排队的线程也可以抢锁
8.synchronized和lock有什么区别
难4出现4
1.语法层面
synchronized是关键字,是c++实现的
Lock是接口,源码是jdk实现的用java语言实现
synchronized退出代码块会同步释放锁,使用Lock必须手动释放调用unLock来释放锁
2.功能层面
都属于悲观锁,都具有互斥同步所重入的功能
Lock提供了许多Syschronized没有的功能,公平锁,可打断,可超时,多条件变量 还有不同场景的实现,ReentrantLock,ReentrantReadWriteLock(读写锁)
3.性能层面
在没有竞争的时候Syschronize很多的优化比如偏向锁,轻量级锁,性能还行。
在竞争激烈的情况下Lock的实现通常会提供更好的性能
9.死锁产生的条件
难3出现3
死锁一个线程需要获得多个锁,就容易发生死锁
- 互斥条件 同一时间只有一个线程可以获取该资源
- 持有并等待条件 线程已经获取了一个资源,同时有在等待其他资源
- 不可剥夺条件 线程获取的资源在完毕之前不能被其他资源剥夺
- 循环等待条件 A等待b的资源 b等待1的资源
破坏其中的一个资源
死锁的问题怎么诊断 jps 和 jstack
jps:输出jvm中运行的进程程序的进程状态信息
jstack:查看java进程中线程的堆栈信息
可视化工具
jconsole:java安装bin目录下直接启动jconsole.exe
10,谈一下ConcurrentHashMap
难3出现4
线程安全的高效的map集合
数据结构采用
- jdk1.7底层采用分段的数组+链表实现
- jdk1.8采用和hashmap一样的数组+链表+红黑树
jdk1.7
jdk1.8
面试回答
1.底层数据结构:
JDK1.7底层采用分段的数组+链表实现
JDK1.8 采用的数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树
2.加锁的方式
JDK1.7采用Seqment分段锁,底层使用的是ReentrantLock
JDK1.8采用KAS添加新节点,采用synchronized锁定链表或红黑二叉树的首节点,相对Segment分段锁粒度更细,性能更好
11.导致并发程序出现问题的根本原因
java并发编程的三大特性
- 原子性 一个线程在cpu中操作是不可以暂停的要嘛全部完成要不不完成
- 可见性 一个线程对共享变量修改后另一个线程可见 加volation
- 有序性 指令重排 加volation
b站黑马程序员面试笔记