Java如何实现线程安全?
线程安全:多线程操作共享数据出现的问题
- 悲观锁:synchronized、lock
- 乐观锁:CAS 当然也可以根据业务情况,使用ThreadLocal,让每个线程玩自己的数据
CAS的底层实现?
CAS就是比较和交换(Compare And Swap):
- 先比较一下内存地址的值与预期值是否一致,如果一致,交换,返回true
- 先比较一下内存地址的值与预期值是否一致,如果不一致,不交换,返回false
底层是调用了Unsafe类中的相关方法(native方法 C++)
CAS底层中,如果是多核的操作系统,需要追加lock前缀指令(单核不需要添加,因为cmpxchg是一行指令,不可再拆分)
lock指令可以理解为CPU层面的锁,锁的粒度就是 缓存行 级别的锁,也有 总线锁 ,成本太高,CPU会根据情况选择
CAS的ABA问题?
假如Thread1和Thread2先后从内存中拿到了值A,这时,Thread1把内存中的值A改成了值B,突然来了Thread3,它拿到内存中的B值后,然后又将内存中的值B改成了A,这个时候使用CAS进行检查时会发现它的值是没有发生变化的,但是实际引用却发生了变化,这就是CAS经典的ABA问题 解决方案: JDK的atomic包中提供了一个类AtomicStampedReference来解决,其中的compareAndSet方法作用首先会检查当前引用是否等于预期的引用,当前标志是否等于预期标志,全部相等的话,则会以原子方式将该引用和该标志的值设置为给定的更新值 以此来解决CAS产生的ABA问题
CAS自旋次数过多?
CAS自旋次数过多,会额外占用大量CPU资源(不会挂起线程)!浪费资源 解决方案:
- synchronized : CAS失败几次后,将线程挂起(WAITING),避免占用CPU过多资源!
- LongAdder:这是基于 分段锁 的形式解决, CAS失败后,将操作的值,存储到Cell数组,需要结果时,将所有值累加,然后返回。
四种引用类型?
- 强引用:User xx = new User(); xx就是强引用,只要引用还在,GC就不会回收!
- 软引用:用一个SofeReference引用的对象,就是软引用,如果内存空间不足,才会回收只有软引用指向的对象。 一般用于做缓存。
- 弱引用:WeakReference引用的对象,一般就是弱引用,只要执行GC,就会回收只有弱引用指向的对象。可以解决内存泄漏的问题 (ThreadLocal)
- 虚引用:PhantomReference引用的对象,就是虚引用,拿不到虚引用指向的对象,一般监听GC回收阶段,或者是回收堆外内存时使用。
Java的内存模型
在处理指令时,CPU会拉取数据,优先级是从L1到L2到L3,如果都没有,需要去主内存中拉取,JMM就是在CPU和主内存之间,来协调,保证可见、有序性等操作。 CPU核心,就是CPU核心(寄存器) 缓存是CPU的缓存,CPU的缓存分为L1(线程独享),L2(内核独享),L3(多核共享) JMM就是Java内存模型的核心,可见性,有序性都基于这实现。 主内存JVM,就是堆内存
保证可见性的方式?
可见性是指线程间,对变量的变化是否可见 保证可见性的方式:
- volatile:用volatile修饰基本数据类型,可以保证每次CPU去操作数据时,都直接去主内存进行读写。
- synchronized:synchronized的内存语义可以保证在获取锁之后,可以保证前面操作的数据是可见的。
- final:常量没法动
volatile修饰引用数据类型
volatile修饰引用数据类型,只能保证引用数据类型的地址是可见的,不保证内部属性可见。
有了MESI协议,为啥还有volatile?
MESI协议和volatile不冲突,因为MESI是CPU层面的,而CPU厂商很多实现不一样,而且CPU的架构中的一些细节也会有影响,比如Store Buffer会影响寄存器写入L1缓存,导致缓存不一致。volatile的底层生成的是汇编的lock指令,这个指令会要求强行写入主内存,并且可以忽略Store Buffer这种缓存从而达到可见性的目的,而且会利用MESI协议,让其他缓存行失效。
volatile的可见性底层实现
volatile的底层生成的是汇编的lock指令,这个指令会要求强行写入主内存,并且可以忽略Store Buffer这种缓存从而达到可见性的目的,而且会利用MESI协议,让其他缓存行失效。
有序性问题
public class Singleton {
private static volatile Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (null == instance) { //1
synchronized (Singleton.class) {
if(null == instance){ //2
singleton = new Singleton();
}
}
}
return instance;
}
单例模式中的懒汉式为了保证线程安全,一般采用DCL方式实现,单采用DCL有几率出现问题。线程可能会拿到初始化一半的对象去操作,可能出现NullPointException。 (初始化对象三步,开辟空间,初始化内部属性,指针指向引用) 在Java编译.java为.class时,会基于JIT做优化,将指令的顺序做调整,从而提升执行效率。 在CPU层面,也会对一些指令进行重新排序,从而提升执行效率。 这种指令的调整,在一些特殊的操作上,会导致出现问题。
volatile的有序性底层实现
被volatile修饰的属性,在编译时,会在前后追加 内存屏障 StoreStore:屏障前的读写操作,必须全部完成,再执行后续操作 StoreLoad:屏障前的写操作,必须全部完成,再执行后续读操作 LoadLoad:屏障前的读操作,必须全部完成,再执行后续读操作 LoadStore:屏障前的读操作,必须全部完成,再执行后续写操作 这只是保证volatile修饰的属性不出现指令重排的问题 CPU是如何实现的呢?
不同的CPU对内存屏障都有一定的支持,比如×86架构,内部自己已经实现了LoadStore,LoadLoad,StoreStore,只针对StoreLoad做了支持。 去openJDK再次查看,mfence是如何支持的。其实在底层还是mfence内部的lock指定,来解决指令重排问题。
synchronized锁升级的过程?
synchronized在JDK 1.6之前,一直是重量级锁:只要线程获取锁资源失败,直接挂起线程 (用户-内核) 在JDK 1.6之前synchronized效率比较低,再加上Doug Lea推出了ReentrantLock,效率比synchronized快多了,导致JDK团队不得不在JDK 1.6将synchronized做优化 锁升级:
- 无锁状态、匿名偏向状态 (也是偏向锁但是不偏向任何线程) :没有线程拿锁
- 偏向锁状态:没有线程的竞争,只有一个线程在获取资源。线程竞争锁资源时,发现当前synchronized没有线程占用锁资源,并且锁是偏向锁,使用CAS的方式,设置o的线程ID为当前线程,获取到锁资源,下次当前线程再次获取时,只需要判断是偏向锁,并且线程ID是当前线程ID即可,直接获得到锁资源。
- 轻量级锁:偏向锁出现竞争时,会升级到轻量级锁(触发偏向锁撤销)。轻量级锁的状态下,线程会基于CAS的方式,尝试获取锁资源,CAS的次数是基于适应自旋锁实现的,JVM会自动的基于上一次获取锁是否成功,来决定这次获取锁资源要CAS多少次。
- 重量级锁:轻量级锁CAS一段次数后,没有拿到锁资源,升级为重量级锁 (其实CAS操作是在重量级锁时执行的)。重量级锁就是线程拿不到锁,就挂起(用户态到内核态之间的切换)。 偏向锁是延迟开启的(默认延迟4秒),并且在开启偏向锁之后,默认不存在无锁状态,只存在匿名偏向状态,synchronized因为不存在从重量级锁降级到偏向锁或者是轻量级锁。 STW:Stop-the-World,简称STW,指的是GC事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应。有点像卡死的感觉,这个停顿成为STW。
synchronized在偏向锁升级到轻量锁时,会涉及到偏向锁撤销,需要等到一个安全点,STW,才可以撤销,并且偏向锁撤销比较消耗资源,在程序启动时,偏向锁有一个延迟开启的操作(默认4秒),因为项目启动时,ClassLoader会加载.class文件,这里会涉及到synchronized操作,为了避免启动时,涉及到偏向锁撤销,导致启动效率变慢,所以程序启动时,默认不是开启偏向锁的。如果在开启偏向锁的情况下,查看对象,默认对象是匿名偏向。
synchronized锁粗化和锁消除?
锁消除: 线程在执行一段synchronized代码块时,发现没有共享数据的操作,自动帮你把synchronized去掉。 锁膨胀(锁粗化):(JIT优化) 在一个多次循环的操作中频繁的获取和释放锁资源,synchronized在编译时,可能会优化到循环外部。
while(){
sync(){
// 多次的获取和释放,成本太高,优化为下面这种
}
}
//----
sync(){
while(){
// 优化成这样
}
}