人人建站网,wordpress 景点模板,上海做兼职上哪个网站,免费学习网站建设并发编程面试题1
一、原子性高频问题#xff1a;
1.1 Java中如何实现线程安全?
多线程操作共享数据出现的问题。
锁#xff1a;
悲观锁#xff1a;synchronized#xff0c;lock乐观锁#xff1a;CAS
可以根据业务情况#xff0c;选择ThreadLocal#xff0c;让每个…并发编程面试题1
一、原子性高频问题
1.1 Java中如何实现线程安全?
多线程操作共享数据出现的问题。
锁
悲观锁synchronizedlock乐观锁CAS
可以根据业务情况选择ThreadLocal让每个线程玩自己的数据。
1.2 CAS底层实现
最终回答先从比较和交换的角度去聊清楚在Java端聊到native方法然后再聊到C中的cmpxchg的指令再聊到lock指令保证cmpxchg原子性
Java的角度CAS在Java层面最多你就能看到native方法。
你会知道比较和交换
先比较一下值是否与预期值一致如果一致交换返回true先比较一下值是否与预期值一致如果不一致不交换返回false
可以去看Unsafe类中提供的CAS操作
四个参数哪个对象哪个属性的内存偏移量oldValuenewValue native是直接调用本地依赖库C中的方法。
https://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/69087d08d473/src/share/vm/prims/unsafe.cpp https://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/69087d08d473/src/os_cpu/linux_x86/vm/atomic_linux_x86.inline.hpp
在CAS底层如果是多核的操作系统需要追加一个lock指令
单核不需要加因为cmpxchg是一行指令不能再被拆分了 看到cmpxchg 是汇编的指令CPU硬件底层就支持 比较和交换 cmpxchgcmpxchg并不保证原子性的。cmpxchg的操作是不能再拆分的指令
所以才会出现判断CPU是否是多核如果是多核就追加lock指令。
lock指令你可以理解为是CPU层面的锁一般锁的粒度就是 缓存行 级别的锁当然也有 总线锁 但是成本太高CPU会根据情况选择。
1.3 CAS的常见问题
ABA ABA不一定是问题因为一些只存在 –的这种操作即便出现ABA问题也不影响结果
线程A期望将value从A1 - B2
线程B期望将value从B2 - A3
线程C期望将value从A1 - C4
按照原子性来说无法保证线程安全。
解决方案很简单Java端已经提供了。 说人话就是在修改value的同时指定好版本号。
JUC下提供的AtomicStampedReference就可以实现。
自旋次数过多
自旋次数过多会额外的占用大量的CPU资源浪费资源。
回答方式可以从synchronized或者LongAdder层面去聊
synchronized方向从CAS几次失败后就将线程挂起WAITING避免占用CPU过多的资源LongAdder方向这里是基于类似 分段锁 的形式去解决要看业务有限制的传统的AtmoicLong是针对内存中唯一的一个值去LongAdder在内存中搞了好多个值多个线程去加不同的值当你需要结果时我将所有值累加返回给你。
只针对一个属性保证原子性 处理方案学了AQS就懂了。ReentrantLock基于AQS实现AQS基于CAS实现核心功能。
1.4 四种引用类型 ThreadLocal的问题
ThreadLocal的问题Java基础面试题2 – 第16题。
四种引用类型 强引用User xx new User(); xx就是强引用只要引用还在GC就不会回收 软引用用一个SofeReference引用的对象就是软引用如果内存空间不足才会回收只有软引用指向对象。 一般用于做缓存。 SoftwareReference xx new SoftwareReference (new User);User user xx.get();弱引用WeakReference引用的对象一般就是弱引用只要执行GC就会回收只有弱引用指向的对象。可以解决内存泄漏的问题 看ThreadLocal即可 ThreadLocal的问题Java基础面试题2 – 第16题。 虚引用PhantomReference引用的对象就是虚引用拿不到虚引用指向的对象一般监听GC回收阶段或者是回收堆外内存时使用。
二、可见性高频问题
2.1 Java的内存模型
回答方式。先全局描述。 在处理指令时CPU会拉取数据优先级是从L1到L2到L3如果都没有需要去主内存中拉取JMM就是在CPU和主内存之间来协调保证可见、有序性等操作。
一定要聊JMM别上来就聊JVM的内存结构不是一个东西Java Memory Model
、
CPU核心就是CPU核心寄存器
缓存是CPU的缓存CPU的缓存分为L1线程独享L2内核独享L3多核共享
JMM就是Java内存模型的核心可见性有序性都基于这实现。
主内存JVM就是你堆内存。
2.2 保证可见性的方式
啥是可见性 可见性是指线程间的对变量的变化是否可见
Java层面中保证可见性的方式有很多
volatile用volatile基本数据类型可以保证每次CPU去操作数据时都直接去主内存进行读写。synchronizedsynchronized的内存语义可以保证在获取锁之后可以保证前面操作的数据是可见的。lockCAS-volatile也可以保证CAS或者操作volatile的变量之后可以保证前面操作的数据是可见的。final是常量没法动~~
2.3 volatile修饰引用数据类型
先说结果 首先volatile修饰引用数据类型只能保证引用数据类型的地址是可见的不保证内部属性可见。
But这个结论只能在hotspot中实现如果换一个版本的虚拟机可能效果就不一样了。volatile修饰引用数据类型JVM压根就没规范过这种操作不同的虚拟机厂商可以自己实现。 这个问题只出现在面试中干活你要这么干……………………干丫的~
2.4 有了MESI协议为啥还有volatile
MESI是CPU缓存一致性的协议大多数的CPU厂商都根据MESI去实现了缓存一致性的效果。
CPU已经有MESI协议了volatile是不是有点多余啊
首先这哥俩不冲突一个是从CPU硬件层面上的一致性一个是Java中JMM层面的一致性。
MESI协议他有一套固定的机制无论你是否声明了volatile他都会基于这个机制来保证缓存的一致性可见性。同时也要清楚如果没有MESI协议volatile也会存在一些问题不过也有其他的处理方案总线锁时间成本太高了如果锁了总线就一个CPU核心在干活。
MESI是协议是规划是interface他需要CPU厂商实现。
既然CPU有MESI了为啥还要volatile那自然是MESI协议有问题。MESI保证了多核CPU的独占cache之间的可见性但是CPU不是说必须直接将寄存器中的数据写入到L1因为在大多是×86架构的CPU中寄存器和L1之间有一个store buffer寄存器值可能落到了store buffer没落到L1中就会导致缓存不一致。而且除了×86架构的CPU在arm和power的CPU中还有load bufferinvalid queue都会或多或少影响缓存一致性
回答的方式MESI协议和volatile不冲突因为MESI是CPU层面的而CPU厂商很多实现不一样而且CPU的架构中的一些细节也会有影响比如Store Buffer会影响寄存器写入L1缓存导致缓存不一致。volatile的底层生成的是汇编的lock指令这个指令会要求强行写入主内存并且可以忽略Store Buffer这种缓存从而达到可见性的目的而且会利用MESI协议让其他缓存行失效。
2.5 volatile的可见性底层实现
volatile的底层生成的是汇编的lock指令这个指令会要求强行写入主内存并且可以忽略Store Buffer这种缓存从而达到可见性的目的而且会利用MESI协议让其他缓存行失效。
三、有序性高频问题
3.1 什么是有序性问题
单例模式中的懒汉机制中就存在一个这样的问题。
懒汉为了保证线程安全一般会采用DCL的方式。
但是单单用DCL依然会有几率出现问题。
线程可能会拿到初始化一半的对象去操作极有可能出现NullPointException。
初始化对象三部开辟空间初始化内部属性指针指向引用
在Java编译.java为.class时会基于JIT做优化将指令的顺序做调整从而提升执行效率。
在CPU层面也会对一些执行进行重新排序从而提升执行效率。
这种指令的调整在一些特殊的操作上会导致出现问题。
3.2 volatile的有序性底层实现
被volatile修饰的属性在编译时会在前后追加 内存屏障 。
SS屏障前的读写操作必须全部完成再执行后续操作
SL屏障前的写操作必须全部完成再执行后续读操作
LL屏障前的读操作必须全部完成再执行后续读操作
LS屏障前的读操作必须全部完成再执行后续写操作 这个内存屏障是JDK规定的目的是保证volatile修饰的属性不会出现指令重排的问题。
volatile在JMM层面保证JIT不重排可以理解但是CPU怎么实现的。
查看这个文档https://gee.cs.oswego.edu/dl/jmm/cookbook.html 不同的CPU对内存屏障都有一定的支持比如×86架构内部自己已经实现了LSLLSS只针对SL做了支持。
去openJDK再次查看mfence是如何支持的。其实在底层还是mfence内部的lock指定来解决指令重排问题。 四、synchronized高频问题
4.1 synchronized锁升级的过程?
锁就是对象随便哪一个都可以Java中所有对象都是锁。
无锁(匿名偏向)、偏向锁、轻量级锁、重量级锁
无锁(匿名偏向) 一般情况下new出来的一个对象是无锁状态。因为偏向锁有延迟在启动JVM的4s中不存在偏向锁但是如果关闭了偏向锁延迟的设置new出来的对象就是匿名偏向。
偏向锁 当某一个线程来获取这个锁资源时此时就会变为偏向锁偏向锁存储线程的ID
当偏向锁升级时会触发偏向锁撤销偏向锁撤销需要等到一个安全点比如GC的时候偏向锁撤销的成本太高所以默认开始时会做偏向锁延迟。
安全点
GC方法返回之前调用某个方法之后甩异常的位置循环的末尾
轻量级锁 当在出现了多个线程的竞争就要升级为轻量级锁有可能直接从无锁变为轻量级锁也有可能从偏向锁升级为轻量级锁轻量级锁的效果就是基于CAS尝试获取锁资源这里会用到自适应自旋锁根据上次CAS成功与否决定这次自旋多少次。
重量级锁 如果到了重量级锁那就没啥说的了如果有线程持有锁其他竞争的就挂起。
4.2 synchronized锁粗化锁消除
锁粗化锁膨胀JIT优化
while(){sync(){// 多次的获取和释放成本太高优化为下面这种}
}
//----
sync(){while(){// 优化成这样}
}锁消除在一个sync中没有任何共享资源也不存在锁竞争的情况JIT编译时就直接将锁的指令优化掉。
4.3 synchronized实现互斥性的原理
偏向锁查看对象头中的MarkWord里的线程ID是否是当前线程如果不是就CAS尝试改如果是就拿到了锁资源。
轻量级锁查看对象头中的MarkWord里的Lock Record指针指向的是否是当前线程的虚拟机栈如果是拿锁执行业务如果不是CAS尝试修改修改他几次不成再升级到重量级锁。
重量级锁查看对象头中的MarkWord里的指向的ObjectMonitor查看owner是否是当前线程如果不是扔到ObjectMonitor里的EntryList中排队并挂起线程等待被唤醒。 4.4 wait为什么是Object下的方法
执行wait方法需要持有sync锁。
sync锁可以是任意对象。
同时执行wait方法是在持有sync锁的时候释放锁资源。
其次wait方法需要去操作ObjectMonitor而操作ObjectMonitor就必须要在持有锁资源的前提的才能操作将当前线程扔到WaitSet等待池中。
同理notify方法需要将WaitSet等待池中线程扔到EntryList如果不拥有ObjectMonitor怎么操作
类锁就是基于类.class作为 类锁
对象锁就是new 一个对象作为 对象锁
设计模式单例工厂代理消费者生产者策略责任链观察者模板装饰者多线程JVMMySQLSpringSpringBootRedisMQ