synchronized与锁机制

  1. Synchronized底层剖析
    1. jdk1.5之前的Synchronized实现
    2. jdk1.5之后的Synchronized实现
      1. CAS操作
    3. 无锁状态
    4. 偏向锁
    5. 轻量级锁
    6. 自旋锁/重量级锁

Synchronized底层剖析

Synchronized俗称对象锁,是一种悲观锁实现,要了解Synchronized的底层机制我们就需要了解象头锁记录的作用,以及jvm是如何去实现不同锁,何时使用不同锁

简单来说,jvm在执行某个线程的时候必然是在某个对象上活跃的,Synchronized作为锁必然离不开某个特定的对象

jdk1.5之前的Synchronized实现

在jdk1.5之前,Synchronized的实现十分简单,其底层是通过内部一个叫做Monitor的监视器实现的,当线程在某个对象上活跃的时候,如果遇到Synchronized关键字就会为这段代码块或是方法上锁,监视器检查到了需要上锁的位置就会调用底层操作系统的Mutex Lock(互斥锁)为需要上锁的资源加锁,此资源的访问就会形成互斥操作

  • 注意:同一时刻,只能有一个线程原有该锁

  • 如果A线程访问某个需要上锁的资源,Monitor就会通过操作系统互斥锁上锁

  • 当B线程访问此资源时就会尝试加锁,只有加锁成功了才能操作如果不成功,则线程阻塞

  • 当A线程释放资源时就会唤醒在此锁上阻塞的所有线程,根据线程优先级获取到新锁(默认:先阻塞,先唤醒)

很显然,在jdk1.5之前的这种上锁机制效率很低,只要遇到资源被占用,线程就会被阻塞,并且通过操作系统实现互斥锁需要从用户态切换到核心态十分耗费系统资源,会带来重大的性能损失

jdk1.5之后的Synchronized实现

jdk1.5之后的Synchronized实现是基于对象实现的

我们知道Java对象中的markword有存放锁的信息,这段信息就是jvm识别在该对象上是否有线程占用的凭据

  • 简单来说markword上的锁信息有以下几种状态
    • 无锁状态
    • 偏向锁
    • 轻量级锁
    • 重量级锁

在这里插入图片描述

参考博客

参考博客

参考博客

CAS操作

Compare And Swap

比较后交换,CAS 是一种无锁的解决方案,是一种基于乐观锁的操作,解决多了线程并行情况下使用锁造成性能损耗

CAS 原子操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)

  • 如果内存位置的值与预期原值一致,则将该值更新为新值

  • 否则,不做任何操作

无锁状态

markword锁标识位01

  • 即对象未被任何线程访问过,锁记录threadId为空

偏向锁

markword锁标识位01,偏向锁1

jvm在开启4s后才会开启偏向锁模式,偏向锁模式下markword的ThreadId为0(匿名偏向状态),偏向锁是一种针对加锁操作的优化手段,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了消除数据在无竞争情况下锁重入(CAS操作)的开销而引入偏向锁

  • 当线程A在一个对象上活跃时,先判断markword是否为无锁状态,偏向锁是否可用,如果无锁jvm会在此对象markword锁记录上记下线程A的threadId,此时我们可以说锁偏向线程A

  • 此线程再次进入的时候只需要判断对象上的锁记录和线程threadId是否相同,相同则线程直接获取锁

  • 如果有线程B访问此对象,则通过CAS操作竞争锁,如果竞争成功,则将对象头锁记录更新为线程B的threadId偏向锁的再偏向机制

    • 如果竞争失败,则说明有已经有别的线程获取到锁了,当达到全局安全点时(在这个时间点上没有字节码正在执行),获取偏向锁的线程被挂起偏向锁被撤销,然后升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块

只有在其它线程通过CAS操作竞争偏向锁的时候,持有该锁的线程才会主动释放偏向锁,从而恢复到无锁状态,尝试锁升级

轻量级锁

markword锁标识位00

轻量级锁是相对于传统通过操作系统互斥量(互斥锁)而言的,它的本意就是在没有多线程竞争的前提下减少传统的重量级锁产生的性能消耗

更新LockRecord和MarkWord

  • 当线程进入同步代码块时,如果对象锁状态为无锁状态,偏向锁标识位为0,则jvm在当前线程栈帧中建立一个名为(Lock Record)的空间,用于存储该对象目前的markword拷贝

  • 拷贝对象头中的Mark Word复制到锁记录中,将对象MarkWord更新为指向Lock Record的指针,并将Lock Record里的owner指针指向对象Mark Word(互指

  • 如果以上操作成功了,则此线程就拥有了该对象的锁(轻量级锁)

  • 如果更新操作失败,则检查MarkWord是否指向该线程,如果指向该线程则说明该线程已经拥有轻量级锁,直接放行

  • 如果不指向该线程则说明存在多个线程同时竞争该锁,轻量级锁就需要膨胀为重量级锁,而当前线程尝试使用自旋来获取锁

    自旋是为了不让线程直接阻塞,采用循环的方式去获取

    如果竞争不激烈且锁占用时间非常短,则自旋锁能提高系统性能

    如果自旋时间过长则会使得大量线程处于CAS重试状态,占用CPU资源

    • 自适应自旋:通过自旋成功的次数调整,成功则次数增加

轻量级锁的解锁,在执行完毕同步代码块时,jvm会将Lock Record之前存储的markword锁记录与当前对象头中的锁记录比较,如果相同则将Lock Record中的锁记录替换回对象头,并且释放轻量级锁

如果不同则表示存在竞争,锁升级为重量级锁

首先只有作为锁的对象处于未被锁定并且CAS成功或者锁被当前线程持有(可重入)时才可以进入同步块,否则都会进入inflate方法,注意在进入inflate方法将mark word中锁标志进行了修改

自旋锁/重量级锁

markword锁标识位10

在轻量级锁膨胀为重量级锁前会尝试使用自旋锁jdk1.7后默认开启

  • 轻量级锁CAS抢占失败,线程将尝试自旋获取锁,如果次数过多则线程挂起进入阻塞,如果占用线程在短时间内释放锁,则阻塞线程被唤醒重新尝试抢占资源

  • 如果多次获取自旋锁失败,同步锁将会升级为重量级锁

  • 未抢到锁的线程都会进入Monitor,之后会被阻塞在WaitSet

下面是一些例子

  • A先进入同步代码块,B随后进入,A执行完毕同步代码块,此时偏向锁属于A且闲置,B尝试获取偏向锁,A在安全点撤销偏向锁,B获取到偏向锁,此时偏向锁属于B

  • A进入同步代码块,B也进入同步代码块,A先获取到偏向锁,此时A未执行完同步代码块,B尝试获取偏向锁,A达到安全点撤销偏向锁并升级锁为轻量级锁,此时B自旋尝试获取锁

    • 如果A短时间内执行完毕,则轻量级锁撤销为无锁状态,B重新获取到轻量级锁
    • 如果A长时间未执行完毕,则轻量级锁膨胀为重量级锁,B阻塞,直到重量级锁撤销,B重新获取轻量级锁

转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以邮件至 1300452403@qq.com

文章标题:synchronized与锁机制

字数:2k

本文作者:Os467

发布时间:2022-11-05, 23:46:13

最后更新:2022-11-05, 23:54:36

原始链接:https://os467.github.io/2022/11/05/synchronized%E4%B8%8E%E9%94%81%E6%9C%BA%E5%88%B6/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

×

喜欢就点赞,疼爱就打赏