yehao
发布于 2023-12-01 / 41 阅读
0
0

synchronized的实现原理

synchronized的实现原理

简介

synchronized 是基于cpu的monitor临界区的上锁。

类型

类锁

synchronized(XXX.class){
	// 代码块
}

所有线程无论是否创建对象,都排他竞争。

对象锁

Object obj = new Object();
synchronized(obj){
	// 代码块
	// 此处在线程内部创建obj,加锁毫无意义。
}

synchronized(this){
	// 代码块
}

每个线程如果都创建一个对象,那么对象锁将无意义(springweb保证对象是单例,平时使用对象锁,可重入)

原理

synchornized底层实现做了多层优化,依赖于cpu提供的montior的能力。

加锁原理

临界区实现同步代码块能力,cas操作当先资源obj(synchornized(obj))设置偏向锁位设置当前线程id。

唤醒等待

synchornized同步代码块中,可以调用wait、notify来使线程等待或唤醒。此方式是montior提供的对运行线程管理的一种方式。

wait会将线程挂起,等待notify调用,重新加入锁的竞争。

锁的优化

当多线程竞争时,synchornized做了底层优化,引入偏向锁轻量锁重量锁

image-escz.png

偏向锁

当只有一个线程进入的时候,会直接将montior中的偏向锁位设置为当前线程id。

轻量级锁

当多个线程参与竞争时,会升级为轻量级锁,spin自旋加入EntrySet竞争。

EntrySet是montior的一种优化,防止过多线程直接竞争锁,而引入的一个链表集合,需要线程先进入EntrySet队列,再进行自旋竞争锁(防止多线程过多消耗资源)。

EntrySet也相当于限流的一种方式。

monitor也方便管理竞争锁线程的状态。

image-xdxp.pngimage-qqtj.png

重量级锁

当在轻量级锁竞争失败,自旋次数达到一定阈值后,会从轻量级锁升级为重量级锁。当前线程会进入挂起阻塞状态。选择合适时机再次自旋竞争。(但是不公平出现,当当前线程进入waitSet阻塞时,新进入的线程可能比它更快速获得锁)

问题

由于CPU架构问题: L1 -> L2 -> L3 -> 内存。

多线程执行下,不同CPU核心在读取变量会先按照 L1 -> L2 -> L3 -> 内存中读取,以便于提高执行效率。由于通常情况下 L1 L2 为cpu核心独有,导致不同线程的数据出现不一致。

java给出的解决方案:

happends-before

常用:

使用volitale修饰共享变量,由jvm禁止CPU对于共享变量的重排序。

synchronized锁定代码块,进入和结束都会也会触发happends-before,并进行强制缓存数据刷新。

jdk9 AtomicRefrence 的tryAcure方法

Thread.start() 一定先于 Thread.run()等

应用

基于synchronized实现的单例

  	private static volatile Singleton instance = null;
    public static Singleton getInstance(){
        if(instance == null){
            synchronized(Singleton.class){
                if(instance == null){
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

疑惑

如果synchronized内部能保证happends-before,在释放锁时,就会把共享变量instance刷新到内存中,那么还需要volatile 修饰吗?

synchronized 能有效避免进入临界区和退出临界区共享变量的值不一致现象,进入和退出都会将共享变量的值刷新到内存,以避免重排序。

但是在代码块内部,创建对象并赋值操作,仍可能出现CPU重排序,先开辟内存空间给instance,再创建对象。此种现象会出现其他线程引用一个未初始化完成的对象。

所以volatile 是必须的。


评论