多线程锁的升级原理

在使用 synchronized 来同步代码块的时候,经编译后,会在代码块的起始位置插入 monitorenter指令,在结束或异常处插入 monitorexit指令。当执行到 monitorenter 指令时,将会尝试获取对象所对应的 monitor 的所有权,即尝试获得对象的锁。而 synchronized 用的锁是存放在 Java对象头 中的。

所以引出了两个关键词:“Java 对象头” 和 “Monitor”

1、Java 对象头

我们以 Hotspot 虚拟机为例,Hotspot 的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针)。

Mark Word:默认存储对象的 HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,所以 Mark Word 被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据。它会根据对象的状态复用自己的存储空间,也就是说在运行期间 Mark Word 里存储的数据会随着锁标志位的变化而变化。

Klass Point:对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。

2、Monitor

Monitor 可以理解为一个同步工具或一种同步机制,通常被描述为一个对象。每一个 Java 对象就有一把看不见的锁,称为内部锁或者 Monitor 锁。

Monitor 是线程私有的数据结构,每一个线程都有一个可用 monitor record 列表,同时还有一个全局的可用列表。每一个被锁住的对象都会和一个 monitor 关联,同时 monitor 中有一个 Owner 字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。

 

锁的级别从低到高:

无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁

img

锁分级别原因:

没有优化以前,synchronized是重量级锁(悲观锁),使用 wait 和 notify、notifyAll 来切换线程状态非常消耗系统资源;线程的挂起和唤醒间隔很短暂,这样很浪费资源,影响性能。所以 JVM 对 synchronized 关键字进行了优化,把锁分为 无锁、偏向锁、轻量级锁、重量级锁 状态。

因为Sycronized是重量级锁(也是悲观锁),每次在要进行锁的请求的时候,如果当前资源被其他线程占有要将当前的线程阻塞加入到阻塞队列,然后清空当前线程的缓存,等到锁释放的时候再通过notify或者notifyAll唤醒当前的线程,并让其处于就绪状态。这样线程的来回切换是非常消耗系统资源的,而且有的时候,线程刚挂起资源就释放了。而Java的线程是映射到操作系统的原生线程之上的每次线程的阻塞或者唤醒都要经过用户态到核心态或者核心态到用户态的转化,这样是十分浪费资源的,这样就会造成性能上的降低,因此JVM对Sychronized进行了优化,将Sycronized分为三种锁的级别:偏向锁,轻量级锁,重量级锁。

无锁:没有对资源进行锁定,所有的线程都能访问并修改同一个资源,但同时只有一个线程能修改成功,其他修改失败的线程会不断重试直到修改成功。

偏向锁:对象的代码一直被同一线程执行,不存在多个线程竞争,该线程在后续的执行中自动获取锁,降低获取锁带来的性能开销。偏向锁,指的就是偏向第一个加锁线程,该线程是不会主动释放偏向锁的,只有当其他线程尝试竞争偏向锁才会被释放。

偏向锁是指当一段同步代码一直被同一个线程所访问时,即不存在多个线程的竞争时,那么该线程在后续访问时便会自动获得锁,从而降低获取锁带来的消耗,即提高性能。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。

当一个线程访问同步代码块并获取锁时,会在 Mark Word 里存储锁偏向的线程 ID。在线程进入和退出同步块时不再通过 CAS 操作来加锁和解锁,而是检测 Mark Word 里是否存储着指向当前线程的偏向锁。轻量级锁的获取及释放依赖多次 CAS 原子指令,而偏向锁只需要在置换 ThreadID 的时候依赖一次 CAS 原子指令即可。

关于偏向锁的撤销,需要等待全局安全点,即在某个时间点上没有字节码正在执行时,它会先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁,恢复到无锁(标志位为01)或轻量级锁(标志位为00)的状态。

偏向锁在 JDK 6 及之后版本的 JVM 里是默认启用的。可以通过 JVM 参数关闭偏向锁:-XX:-UseBiasedLocking=false,关闭之后程序默认会进入轻量级锁状态。

当锁对象第一次被线程获取的时候,虚拟机会将锁对象的对象头中的锁标志位设置成为01,并将偏向锁标志设置为1,线程通过CAS的方式将自己的ID值放置到对象头中(因为在这个过程中有可能会有其他线程来竞争锁,所以要通过CAS的方式,一旦有竞争就会升级为轻量级锁了),如果成功线程就获得了该轻量级锁。这样每次再进入该锁对象的时候不用进行任何的同步操作,直接比较当前锁对象的对象头是不是该线程的ID,如果是就可以直接进入。

偏向锁升级为轻量级锁 偏向锁是一种无竞争锁,一旦出现了竞争大多数情况下就会升级为轻量级锁。现在我们假设有线程1持有偏向锁,线程2来竞争偏向锁会经历以下几个过程:

  1. 首先线程2会先检查偏向锁标记,如果是1,说明当前是偏向锁,那么JVM会找到线程1,看线程1是否还存活着

  2. 如果线程1已经执行完毕,就是说线程1不存在了(线程1自己是不会去释放偏向锁的),那么先将偏向锁置为0,对象头设置成为无锁的状态,用CAS的方式尝试将线程2的ID放入对象头中,不进行锁升级,还是偏向锁

  3. 如果线程1还活着,先暂停线程1,将锁标志位变成00(轻量级锁)然后在线程1的栈帧中开辟出一块空间(Display Mark Word)将对象头的Mark Word置换到线程一的栈帧当中,而对象头中此时存储的是指向当前线程栈帧的指针。此时就变成了轻量级锁。继续执行线程1,然后线程2采用CAS的方式尝试获取锁。

偏向锁的撤销,需要在某个时间点上没有字节码正在执行时,先暂停拥有偏向锁的线程,然后判断锁对象是否处于被锁定状态。如果线程不处于活动状态,则将对象头设置成无锁状态,并撤销偏向锁;

如果线程处于活动状态,升级为轻量级锁的状态。

轻量级锁与偏向锁最大的不同之处 轻量级锁和偏向锁的不同之处就在于轻量级锁对于获取锁对象采用CAS的同步方式而偏向锁直接是把整个同步过程给取消。

轻量级锁:轻量级锁是指当锁是偏向锁的时候,被第二个线程 B 所访问,此时偏向锁就会升级为轻量级锁,线程 B 会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。

轻量级锁是指当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其他线程会通过自旋(关于自旋的介绍见文末)的形式尝试获取锁,线程不会阻塞,从而提高性能。

轻量级锁如何创建在上面已经讲过了,接下来说说轻量级锁如何获取锁对象,轻量级锁是通过CAS也就是自旋的方式尝试获取锁对象,一旦失败会先检查,对象头中存储的是否是指向当前线程栈帧的指针,如果是,就可以获取对象,如果不是说明存在竞争那么就要膨胀为重量级锁。轻量级锁的解锁也是通过CAS的方式尝试将对象头的Mark Word和线程中的Display Mark Word替换回来,如果成功,就释放锁,如果失败说明还有许多其他等待锁的线程(说明此时已经不是轻量级锁而是重量级锁了),会将这些线程唤醒,然后释放锁。

当前只有一个等待线程,则该线程将通过自旋进行等待。但是当自旋超过一定的次数时,轻量级锁便会升级为重量级锁;当一个线程已持有锁,另一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁。

在代码进入同步块的时候,如果同步对象锁状态为无锁状态,虚拟机将首先在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的 Mark Word 的拷贝,然后将对象头中的 Mark Word 复制到锁记录中。

拷贝成功后,虚拟机将使用 CAS 操作尝试将对象的 Mark Word 更新为指向 Lock Record 的指针,并将 Lock Record 里的 owner 指针指向对象的 Mark Word。

如果这个更新动作成功了,那么这个线程就拥有了该对象的锁,并且对象 Mark Word 的锁标志位设置为“00”,表示此对象处于轻量级锁定状态。

如果轻量级锁的更新操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明多个线程竞争锁。

重量级锁:指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。

重量级锁通过对象内部的监视器(monitor)实现,而其中 monitor 的本质是依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态切换到内核态,切换成本非常高。

一旦有两条以上的线程竞争锁,轻量级锁膨胀为重量级锁,锁的状态变成10,此时对象头中存储的就是指向重量级锁的栈帧的指针。而且其他等待锁的线程要进入阻塞状态,等待重量级锁释放再来被唤醒然后去竞争。

锁状态对比:

 偏向锁轻量级锁重量级锁
适用场景只有一个线程进入同步块虽然很多线程,但是没有冲突:多条线程进入同步块,但是线程进入时间错开因而并未争抢锁发生了锁争抢的情况:多条线程进入同步块并争用锁
本质取消同步操作CAS操作代替互斥同步互斥同步
优点不阻塞,执行效率高(只有第一次获取偏向锁时需要CAS操作,后面只是比对ThreadId)不会阻塞不会空耗CPU
缺点适用场景太局限。若竞争产生,会有额外的偏向锁撤销的消耗长时间获取不到锁空耗CPU阻塞,上下文切换,重量级操作,消耗操作系统资源
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值