Synchnorized原理详解

synchronized概述

synchronized关键字最主要有以下3种应用方式:

  1. 修饰非静态方法(或者叫实例方法),调用该方法的当前实例充当锁;
  2. 修饰静态方法,类对象充当锁;
  3. 修饰代码块,要指定加锁对象。

synchronized的实现原理概述:
(1)synchronized同步代码块:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-f3uVFtYk-1657792780499)(network-img/image-20220714144212684.png)]

在synchronized同步代码块中,synchronized关键字经过编译之后,会在同步代码块前后分别形成monitorenter和monitorexit字节码指令,在执行monitorenter指令的时候,首先尝试获取对象锁,如果这个锁没有被锁定或者当前线程已经拥有了那个对象的锁,锁的计数器就加1,在执行monitorexit指令时会将锁的计数器减1,当减为0的时候就释放锁。如果获取对象锁一直失败,那当前线程就要阻塞等待,直到对象锁被另一个线程释放为止。(一个monitorenter要与一个monitorexit匹配,为了保证在方法异常时 monitorenter 和 monitorexit 指令依然可以正确配对执行,字节码中有两个monitorexit指令,即异常结束时被执行释放monitor)

(2)synchronized同步方法: synchronized修饰方法时,方法级的同步是隐式的,无须通过字节码指令来控制,JVM可以从方法常量池的方法表结构中的ACC_SYNCHRONIZED访问标志得知一个方法是否声明为同步方法。当方法调用的时,调用指令会检查方法的ACC_SYNCHRONIZED访问标志是否被设置,如果设置了,执行线程就要求先持有monitor对象,然后才能执行方法,最后当方法执行完(无论是正常完成还是非正常完成)时释放monitor对象。方法执行期间,执行线程持有了monitor,其他任何线程都无法再获得同一个monitor

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5CjKge0C-1657792780500)(network-img/image-20220708145911343.png)]

如上图,线程执行synchronized时对象充当着锁,对象的对象头中保存着对应的monitor对象,monitor对象的_object保存着这个对象,当monitor对象中的_owner属性指向当前线程,线程抢占锁成功。

锁的是什么:我们可以使用wait notify notifyAll这些操作来唤醒线程和挂起线程,但是这些方法只能在synchronized内使用,而且wait notify notifyAll这些操作是Object操作类的,,而且wait notify notifyAll这些方法其实就是操作monitor,即锁住x对象获取对象x相关联的监视器

接下来看看monitor具体执行过程

monitor监视器

在HotSpot虚拟机中,monitor是由ObjectMonitor实现的。其源码是用c++来实现的,位于HotSpot虚拟机源码ObjectMonitor.hpp文件中(srclshare/vm/runtime/objectMonitor.hpp)。ObjectMonitor主要数据结构如下:

ObjectMonitor() {
    _header       = NULL;
    _count        = 0;   //记录个数
    _waiters      = 0,
    _recursions   = 0;   //线程的重入次数
    _object       = NULL;//存储该monitor的对象
    _owner        = NULL;//标识拥有该monitor的线程
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet(如个线程进入到同步代码块,执行obj.wait()进入阻塞,就会加入_WaitSet指向的等待队列中)
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;//多线程竞争锁时,没有抢到锁的线程会加入到_cxq指向的单向列表
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表(第一次没抢占到先进入_cxq,如果第二次还没抢到就会进入EntryList中)
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

monitor竞争

1.执行monitorenter时,会调用InterpreterRuntime.cpp
(位于: src/share/vm/interpreter/interpreterRuntime.cpp)的InterpreterRuntime:monitorenter函数。具体代码可参见HotSpot源码。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-tfWpdX1V-1657792780501)(network-img/image-20220714150534675.png)]

2.对于重量级锁,monitorenter函数中会调用ObjectSynchronizer :: slow_enter,最终调用QbjectMonitor :: enter(位于: src/share/vm/runtime/objectMonitor.cpp)获取锁,源码如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2zkAaJGA-1657792780502)(network-img/image-20220714150747235.png)]

此处省略锁偏向、自旋优化等操作,统一放在后面synchronzied优化中说。以上代码的具体流程概括如下:
1.通过CAS尝试把monitor的_owner字段设置为当前线程。
⒉如果设置之前的_owner指向当前线程,说明当前线程再次进入monitor,即重入锁,执行recursions ++,记录重入的次教。

3.如果当前线程是第一次进入该monitor,设置recursions为1,_owner为当前线程,该线程成功获得锁并返回。
4.如果获取锁失败,则等待其他线程释放锁。

monitor等待

竞争到monitor的线程继续往下执行,竞争不到的会执行调用的是ObjectMonitor对象的Enterl方法(位于:src/share/vm/runtime/objectMonitor.cpp)源码如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-x0mcsb89-1657792780502)(network-img/image-20220714151039206.png)]

当该线程被唤醒时,会从挂起的点继续执行,通过objectMonitor : : TryLock.尝试获取锁,TryLock方法实现如下:
在这里插入图片描述

以上代码的具体流程概括如下:
1.当前线程被封装成ObjectWaiter对象node,状态设置成ObjectWaiter::TS_CXQ。
2.在for循环中,通过CAS把node节点push到_cxq列表中,同一时刻可能有多个线程把自己的node节点push到_cxq列表中。
3.node节点push到_cxq列表之后,通过自旋尝试获取锁,如果还是没有获取到锁,则通过park将当前线程挂起,等待被唤醒。
4.当该线程被唤醒时,会从挂起的点继续执行,通过objectMonitor : :TryLock尝试获取锁。

monitor释放

当某个持有锁的线程执行完同步代码块时,会进行锁的释放,给其它线程机会执行同步代码,在HotSpot中
通过退出monitor的方式实现锁的释放,并通知被阻塞的线程,具体实现位于ObjectMonitor的exit方法中。(位于:srclshare/vm/runtime/objectMonitor.cpp),源码如下所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lYNJHPp9-1657792780503)(network-img/image-20220714151635513.png)]

唤醒时,根据QMode策略唤醒:
QMode=2,取cxq头部节点直接唤醒
QMode=3,如果cxq非空,把cxq队列放置到entrylist的尾部(顺序跟cxq一致)
QMode=4,如果cxq非空,把cxq队列放置到entrylist的头部(顺序跟cxq相反)
QMode=0,啥都不做,继续往下走(QMode默认是0)默认是0
Qmode=0的判断逻辑就是先判断entrylist是否为空,如果不为空,则取出第一个唤醒,如果为空再从cxq里面获取第一个唤醒

1.退出同步代码块时会让_recursions减1,当_recursions的值减为0时,说明线程释放锁。
2.根据不同的策略(由QMode指定),从cxq或EntryList中获取头节点,通过objectMonitor ::ExitEpilog方法唤醒该节点封装的线程,唤醒操作最终由unpark完成,实现如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Au774QTJ-1657792780504)(network-img/image-20220714151731832.png)]

被唤醒的线程,会回到void ATTR objectMonitor ::EnterI (TRAPs)的第600行,继续执行monitor的竞争。如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1R1BHpWm-1657792780505)(network-img/image-20220714151822153.png)]

小结:

上述整个过程总结

synchronized的实现原理:

在执行monitorenter指令或者获取到同步方法的标识ACC_SYNCHRONIZED时,首先从对象头中找到关联的监视器monitor对象,通过CAS尝试把monitor的_owner字段设置为当前线程,如果当前线程是第一次进入该monitor,设置recursions为1,_owner为当前线程,该线程成功获得锁并返回;

如果设置之前的_owner指向当前线程,说明当前线程再次进入monitor,即重入锁,执行recursions ++,记录重入的次数;

如果获取锁失败,则等待其他线程释放锁。

等待时当前线程被封装成ObjectWaiter对象,通过CAS把该节点push到_cxq等待队列中后,再次通过cas自旋尝试获取锁,如果还是没有获取到锁,则将当前线程挂起,等待被唤醒。

当该线程被唤醒时,会从挂起的点继续执行尝试获取锁。

执行到monitorexit指令时,或同步方法执行完,_recursions减1,当_recursions的值减为0时,说明线程释放锁。

然后又从cxq或EntryList中获取头节点,唤醒下一个线程执行。

Synchnorized优化

Linux体系架构

上述锁的释放和获取过程中,可以看到ObjectMonitor的函数调用中会涉及到Atomic::cmpxchg_ptr,Atomic:inc_ptr等内核函数,没有竞争到锁的对象会park()被挂起,竞争到锁的线程会unpark()唤醒。这些操作存在操作系统用户态和内核态的转换,这种切换会消耗大量的系统资源。所以synchronized是Java语言中是一个重量级(Heavyweight)的操作。

首先来了解什么是用户态和和内核态,为什么用户态和内核态的转换开销大。

如下图Linux系统的体系架构:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Zl5gTY5D-1657792780505)(network-img/image-20220713235434075.png)]

从上图可以看出,Linux操作系统的体系架构分为:用户空间(应用程序的活动空间)和内核。

**内核:**本质上可以理解为一种软件,控制计算机的硬件资源,并提供上层应用程序运行的环境。
**用户空间:**上层应用程序活动的空间。应用程序的执行必须依托于内核提供的资源,包括CPU资源、存储资源、I/O资源等。
**系统调用:**为了使上层应用能够访问到这些资源,内核必须为上层应用提供访问的接口:即系统调用。

为了保证系统安全,有些指令不能随便被执行去操作底层资源,这些内核指令只能在空间执行,普通执行在用户空间执行,但有时用户程序中可能涉及到内核指令的调用,此时就需要陷入内核中运行,CPU从用户态转换到内核态。

系统调用的过程可以简单理解为:
1.用户态程序将一些数据值放在寄存器中,或者使用参数创建一个堆栈,以此表明需要操作系统提供的服务。

2.用户态程序执行系统调用。
3.CPU切换到内核态,并跳到位于内存指定位置的指令。
4.系统调用处理器(system call handler)会读取程序放入内存的数据参数,并执行程序请求的服务。

5.系统调用完成后,操作系统会重置CPU为用户态并返回系统调用的结果。

由此可见用户态切换至内核态需要传递许多变量,同时内核还需要保护好用户态在切换时的一些寄存器值、变量等,以备内核态切换回用户态。这种切换就带来了大量的系统资源消耗,这就是在synchronized未优化之前,效率极低的原因。

synchronized依赖于对象头,所以先来了解哈java对象布局。

java对象布局

在JVM中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。如下图所示:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-oFixjRrD-1657792780506)(network-img/image-20220714155857685.png)]

对象头

HotSpot采用instanceOopDesc和arrayOopDesc来描述对象头,instanceOopDesc普通对象的对象头,arrayOopDesc:数组对象的对象头。instanceOopDesc的定义的在Hotspot源码的 instanceOop.hpp文件中,另外,arrayOopDesc的定义对应arrayOop.hpp 。

在这里插入图片描述

从instanceOopDesc代码中可以看到 instanceOopDesc继承自oopDesc,oopDesc的定义在Hotspot源码中的oop.cpp文件中。

在这里插入图片描述

在普通实例对象中,对象头由两部分组成,一部分用于存储自身的运行时数据,称之为Mark Word,另外一部分是类型指针,指向堆中大的Class对象,作为该对象元数据的访问入口。

Mark Word

Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等等,占用内存大小与虚拟机位长一致。Mark Word对应的类型是markOop。源码位于markoop.hpp 中。如下:

在这里插入图片描述

上述展示了32位和64位的结构,64位比较常见,这里讨论64位。上面的源码描述可用下图表示

在这里插入图片描述

klass pointer

这一部分用于存储对象的类型指针,该指针指向它的类元数据,JVM通过这个指针确定对象是哪个类的实例。

该指针的位长度为JVM的一个字大小,即32位的JVM为32位,64位的JVM为64位。
如果应用的对象过多,使用64位的指针将浪费大量内存,统计而言,64位的JVM将会比32位的JVM多耗费50%的内存。为了节约内存可以使用选项-xX:+UseCompressedoops开启指针压缩,其中,oop即ordinary objectpointer普通对象指针。开启该选项后,下列指针将压缩至32位:
1.每个Class的属性指针(即静态变量)
2.每个对象的属性指针(即对象变量)

​ 3.普通对象数组的每个元素指针
当然,也不是所有的指针都会压缩,一些特殊类型的指针JVM不会优化,比如指向PermGen的Class对象指针(JDK8中指向元空间的Class对象指针)、本地变量、堆栈元素、入参、返回值和NULL指针等。

对象头=Mark Word +类型指针(未开启指针压缩的情况下)
在32位系统中,Mark Word = 4 bytes,类型指针=4bes,对象头=8 bytes = 64 bits;

在64位系统中,Mark Word = 8 bytes,类型指针=8bytes,对象头=16 bytes = 128bits;

实例数据

类中定义的成员变量。

对齐填充:

由于HotSpot VM的自动内存管理系统要求对象起始地址必须是8字节的整数倍,即对象的大小必须是8字节的整数倍。而对象头正好是8字节的倍数,因此,当对象实例数据部分没有对齐时,就需要通过对齐填充来补全。

Java对象由3部分组成,对象头,实例数据,对齐数据

对象头分成两部分:Mark World + Klass pointer

偏向锁

![在这里插入图片描述](https://img-blog.csdnimg.cn/bd0ca9c064654a6cba53d9bf9e1f5715.png

偏向锁是JDK1.6中的重要引进,因为HotSpot作者经过研究实践发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁。
这个锁会在对象头存的Thread中保存偏向的线程ID,以后该线程进入和退出同步块时只需要检查是否为偏向锁、锁标志位以及ThreadlD即可。

偏向锁原理
当线程第一次访问同步块并获取锁时,偏向锁处理流程如下:
1.虚拟机将会把对象头中的标志位设为"01",即偏向模式。
2.同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word的Thread中,如果CAS操作成功,则获取锁成功。失败,则存在锁竞争。

持有偏向锁的线程以后每次进入这个锁相关的同步块时,只需要检查是否为偏向锁、锁标志位以及ThreadlD,不用别的操作,效率高。

如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YGwsnBvo-1657792780511)(network-img/image-20220709110740135.png)]

第一次循环进入sync同步代码块,会将偏向锁标识设置为1,ThreadId指向当前线程,第二次进入循环,发现是偏向锁,然后比较ThreadId还是t1,则继续占用锁执行,以后也是这样

如果反复是同一个线程则适合用偏向锁,但一旦有多个线程来竞争,就要撤销偏向锁,升级为轻量级锁

偏向锁的撤销:偏向锁的释放采用了一种只有竞争才会释放锁的机制,线程是不会主动去释放偏向锁,需要等待其他线程来竞争。偏向锁的撤销需要等待全局安全点(这个时间点是上没有正在执行的代码)。
1.偏向锁的撤销动作必须等待全局安全点
⒉暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态
3.撤销偏向锁,恢复到无锁(标志位为01)或轻量级锁(标志位为00)的状态

偏向锁在Java 1.6之后是默认启用的,但在应用程序启动几秒钟之后才激活,可以使用-
xx:BiasedLockingStartupDelay=0参数关闭延迟,如果确定应用程序中所有锁通常情况下处于竞争状态,可以通过xx:-UseBiasedLocking=false参数关闭偏向锁。

偏向锁好处
偏向锁适用于一个线程反复获得同一锁的情况。偏向锁可以提高带有同步但无竞争的程序性能。
但它并不一定总是对程序运行有利,如果程序中大多数的锁总是被多个不同的线程访问,偏向锁是没有用的,反而影响效率,因为撤销偏向锁要等待全局安全点才能撤销,所有线程都会停下,反而浪费性能

轻量级锁

轻量级锁是JDK1.6之中加入的新型锁机制,轻量级锁并不是用来代替重量级锁的,它适用于多线程交替执行同步块的情况下。

但是如果多个线程在同一时刻进入临界区,会导致轻量级锁膨胀升级重量级锁

当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁

其步骤如下:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-viJ12XLU-1657792780511)(network-img/image-20220714164815321.png)]

1.判断当前对象是否处于无锁状态.,如果是,则JVM首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀,即Displaced Mark Word),将对象的Mark Word复制到栈帧中的Lock Record中,将LockReocrd中的owner指向当前对象。
2.JVM利用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针,如果成功表示竞争到锁,则将锁标志位变成00,执行同步操作。

3.如果失败则判断当前对象的Mark Word是否指向当前线程的栈帧,如果是则表示当前线程已经持有当前对象的锁,则直接执行同步代码块;否则只能说明该锁对象已经被其他线程抢占了,当前线程便尝试使用自旋来获取锁。

轻量级锁的释放

轻量级锁的释被也是通过CAS操作来进行的。

主要步骤如下:

1.取出在获取轻量级锁保存在Displaced Mark Word中的数据。
2.用CAS操作将取出的数据替换当前对象的Mark Word中,如果成功,则说明释放锁成功,
3.如果CAS操作替换失败,说明有其他线程尝试获取该锁,则需要将轻量级锁需要膨胀升级为重量级锁。

在多线程交替执行同步块的情况下,可以避免重量级锁引起的性能消耗。但轻量级锁不是在任何时候开销都比较少,其性能提升的依据是"对于绝大部分的锁,在整个生命周期内都是不会存在竞争的”,如果打破这个依据则除了互斥的开销外,还有额外的CAS操作,因此在有多线程竞争的情况下,轻量级锁比重量级锁更慢。

轻量级锁怎样实现锁重入

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cNbHe4RJ-1657792780512)(network-img/u=3204721458,3486761604&fm=253&fmt=auto&app=138&f=JPG.jpeg)]

当轻量级锁已经被线程持有,且对象头的Mark Word指向的是当前线程的栈帧时,会把本条Lock RecordDisplaced Mark Word 设置为 null,实现锁重入。当重入解锁时,只需要修改所有者onwer的指向。

自旋与自适应自旋

前面知道线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,可能有些任务执行时间比较短,线程1线程2来竞争执行,假如线程1正在执行,线程2没抢到锁进入阻塞,但刚阻塞线程1已经执行完了,这样影响性能,要是让2在等一下就能执行了,省去阻塞唤醒。所以获取轻量级锁失败后,还会通过自旋尝试获取锁。如果自旋之后依然没有获取到锁,也就只能升级为重量级锁了。

如果持有锁的线程能在很短时间内释放锁资源,就可以让线程执行一个忙循环(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。但是线程自旋需要消耗cpu的资源,如果一直得不到锁就会浪费cpu资源。(自旋锁次数的默认值是10次,用户可以使用参数-XX:PreBlockSpin来更改)因此在jdk1.6引入了自适应自旋锁,自旋等待的时候不固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HpVqXKld-1657792780512)(network-img/image-20220709141636436.png)]

重量级锁

轻量级锁失败后,通过自旋尝试获取锁,还是失败,则升级为重量级锁了。

重量级锁开销大,尽量避免重量级锁

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0VGnJOD8-1657792780513)(network-img/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2ppbmppbmlhbzE=,size_16,color_FFFFFF,t_70.png)]

锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。

其他优化:

锁消除
锁消除是指虚拟机即时编译器在运行时,对于一些代码上要求同步但是被检测不可能存在共享数据竞争的锁进行消除。例如String类型的连接操作。

public class Demoo1 {
    public static void main(String[] args) {
    	contactstring( "aa""bb""cc");
    }
    public static String contactstring(String s1, String s2, String s3){
    	return new StringBuffer ().append(s1).append(s2).append(s3).toString();
    }
}

上述StringBuffer的append方法是同步方法,new的StringBuffer()对象是局部变量,没有发生逃逸,就算多线程来执行,拿到的是不同的StringBuffer,即不同的锁,不存在数据共享,所以同步代码块没必要,在经过即时编译器编译之后就会忽略掉所有的同步直接执行。

锁粗化

如果一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即使没有线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。所以如果虚拟机检测到有一系列的连续操作都是对同一个对象反复加锁和解锁,就会将其合并成一个更大范围的加锁和解锁操作。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Dm55V5PJ-1657792780513)(network-img/image-20220709142939164.png)]

上述中一般一百个同步代码快嵌套,消耗也比较大,一般会把append的sync抹掉,加在循环外面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q9HUx0Lq-1657792780514)(network-img/image-20220709143100600.png)]

锁升级总结:

锁会随着线程的竞争情况逐渐升级,偏向锁 => 轻量级锁 => 重量级锁 。锁可以升级但是不能降级。升级的目的是为了提高获得锁和释放锁的效率。

偏向锁

当线程第一次访问同步块并获取锁时,虚拟机将会把对象头中的标志位设为"01",即偏向模式,同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word的Thread中,如果CAS操作成功,则获取锁成功。以后该线程再进入和推出同步块时不需要进行CAS(比较和交换,下次详细记录一下)操作来加锁和解锁。通过判断对象头的Mark Word里面是否存有指向当前线程的偏向锁来决定是否需要使用CAS竞争锁。

适用场景:锁反复被同一个线程获取的场景,如果系统中大多数情况存在线程竞争,建议关闭偏向锁,因为开启反而会因为偏向锁撤销操作而引起更多的资源消耗。

可以通过JVM参数关闭偏向锁:

-XX:- UseBiasedLocking=false

程序默认会进入轻量级锁状态。

轻量级锁

加锁

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word,将LockReocrd中的owner指向当前对象。然后线程尝试使用 CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,将锁标志位变成00,执行同步操作。如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

轻量级锁的释放

取出在获取轻量级锁保存在Displaced Mark Word中的数据,用CAS操作将取出的数据替换当前对象的Mark Word中,如果成功,则说明释放锁成功,如果CAS操作替换失败,说明有其他线程尝试获取该锁,则需要将轻量级锁需要膨胀升级为重量级锁。

适用场景:少量线程交替获取锁,同步块执行速度非常快的场景。追求响应时间。

优缺点对比

偏向锁

​ 优:加锁/解锁不需要额外的消耗

​ 缺:有竞争时,会有额外的撤销锁或升级锁的消耗

轻量级锁

​ 优:竞争的线程不会被阻塞(采用自旋),提高了程序的响应速度

​ 缺:始终得不到锁的线程一直自旋会消耗cpu,造成cpu浪费(自旋好像就是无实际意义的循环,可以设定一个自旋等待的最大时间)

重量级锁

​ 优:线程竞争不使用自旋,线程竞争锁失败后会阻塞,cpu的消耗会减少,增大了数据的吞吐量

​ 缺:线程阻塞,响应速度慢

适用场景:大量线程同时竞争锁,追求吞吐量。

平时写代码如何对synchronized优化

减少synchronized的范围
同步代码块中尽量短,减少同步代码块中代码的执行时间,减少锁的竞争。

降低锁粒度

将一个锁拆分为多个锁提高并发度: 如hashtable锁整张表,correntHashMap锁一个节点

使用不同的锁

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-V2UCyBWK-1657792780514)(network-img/image-20220714173119362.png)]

读写分离

读取时不加锁,写入和删除时加锁
ConcurrentHashMap,CopyOnWriteArrayList和ConyOnWriteSet

参考:
Synchronized原理(轻量级锁篇)
为什么要有轻量级锁

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值