synchronized四种锁状态的升级

1、背景

在 Java 语言中,使用 Synchronized 是可以实现线程同步的,即加锁。而且实现的是悲观锁,在操做同步资源的时候直接先加锁。html

加锁可使一段代码在同一时间只有一个线程能够访问,在增长安全性的同时,牺牲掉的是程序的执行性能,因此为了在必定程度上减小得到锁和释放锁带来的性能消耗,在 jdk6 以后便引入了“偏向锁”和“轻量级锁”,因此总共有4种锁状态,级别由低到高依次为:无锁状态偏向锁状态轻量级锁状态重量级锁状态。这几个状态会随着竞争状况逐渐升级。java

注意:锁能够升级但不能降级。安全

锁状态说明及升级图示

固然了,在谈这四种状态以前,咱们仍是有必要再简单了解下 synchronized 的原理。数据结构

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

因此引出了两个关键词:“Java 对象头” 和 “Monitor”。性能

2、Java 对象头和 Monitor

一、Java 对象头

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

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

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

二、Monitor

Monitor 能够理解为一个同步工具或一种同步机制,一般被描述为一个对象。每个 Java 对象就有一把看不见的锁,称为内部锁或者 Monitor 锁。设计

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

3、无锁

无锁是指没有对资源进行锁定,全部的线程都能访问并修改同一个资源,但同时只有一个线程能修改为功。

无锁的特色是修改操做会在循环内进行,线程会不断的尝试修改共享资源。若是没有冲突就修改为功并退出,不然就会继续循环尝试。若是有多个线程修改同一个值,一定会有一个线程能修改为功,而其余修改失败的线程会不断重试直到修改为功。

4、偏向锁

偏向锁是指当一段同步代码一直被同一个线程所访问时,即不存在多个线程的竞争时,那么该线程在后续访问时便会自动得到锁,从而下降获取锁带来的消耗,即提升性能。

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

偏向锁只有遇到其余线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。

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

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

5、轻量级锁

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

轻量级锁的获取主要由两种状况:① 当关闭偏向锁功能时;② 因为多个线程竞争偏向锁致使偏向锁升级为轻量级锁。

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

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

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

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

若当前只有一个等待线程,则该线程将经过自旋进行等待。可是当自旋超过必定的次数时,轻量级锁便会升级为重量级锁(锁膨胀)。

另外,当一个线程已持有锁,另外一个线程在自旋,而此时又有第三个线程来访时,轻量级锁也会升级为重量级锁(锁膨胀)。

6、重量级锁

重量级锁是指当有一个线程获取锁以后,其他全部等待获取该锁的线程都会处于阻塞状态。

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

简言之,就是全部的控制权都交给了操做系统,由操做系统来负责线程间的调度和线程的状态变动。而这样会出现频繁地对线程运行状态的切换,线程的挂起和唤醒,从而消耗大量的系统资源,致使性能低下。

7、关于自旋

关于自旋,简言之就是让线程喝杯咖啡小憩一下,用代码解释就是:

do  {
    // do something
}  while  (自旋的规则,或者说自旋的次数)
复制代码

引入自旋这一规则的缘由其实也很简单,由于阻塞或唤醒一个 Java 线程须要操做系统切换 CPU 状态来完成,这种状态转换须要耗费处理器时间。若是同步代码块中的内容过于简单,状态转换消耗的时间有可能比用户代码执行的时间还要长。而且在许多场景中,同步资源的锁定时间很短,为了这一小段时间去切换线程,这部分操做的开销实际上是得不偿失的。

因此,在物理机器有多个处理器的状况下,当两个或以上的线程同时并行执行时,咱们就可让后面那个请求锁的线程不放弃 CPU 的执行时间,看看持有锁的线程是否很快就会释放锁。而为了让当前线程“稍等一下”,咱们需让当前线程进行自旋。若是在自旋完成后前面锁定同步资源的线程已经释放了锁,那么当前线程就能够没必要阻塞而是直接获取同步资源,从而避免切换线程的开销。

自旋锁自己是有缺点的,它不能代替阻塞。自旋等待虽然避免了线程切换的开销,但它要占用处理器时间。若是锁被占用的时间很短,自旋等待的效果就会很是好。反之,若是锁被占用的时间很长,那么自旋的线程只会白浪费处理器资源。

因此,自旋等待的时间必需要有必定的限度,若是自旋超过了限定次数(默认是10次,可使用 -XX:PreBlockSpin 来更改)没有成功得到锁,就应当挂起线程。

自旋锁在 JDK1.4.2 中引入,使用 -XX:+UseSpinning 来开启。JDK 6 中变为默认开启,而且引入了自适应的自旋锁(适应性自旋锁)。

自适应自旋锁意味着自旋的时间(次数)再也不固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。若是在同一个锁对象上,自旋等待刚刚成功得到过锁,而且持有锁的线程正在运行中,那么虚拟机就会认为此次自旋也是颇有可能再次成功,进而它将容许自旋等待持续相对更长的时间。若是对于某个锁,自旋不多成功得到过,那在之后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

8、总结

偏向锁经过对比 Mark Word 解决加锁问题,避免执行CAS操做。

轻量级锁是经过用 CAS 操做和自旋来解决加锁问题,避免线程阻塞和唤醒而影响性能。

重量级锁是将除了拥有锁的线程之外的线程都阻塞。

原文地址:www.jetchen.cn/synchronize…

参考:tech.meituan.com/2018/11/15/…

相关文章
相关标签/搜索