前面两篇文章我介绍了一下html
看完你就会知道,线程若是锁住了某个资源,导致其余线程没法访问的这种锁被称为悲观锁,相反,线程不锁住资源的锁被称为乐观锁,而自旋锁是基于 CAS 机制实现的,CAS又是乐观锁的一种实现,那么对于锁来讲,多个线程同步访问某个资源的流程细节是否同样呢?换句话说,在多线程同步访问某个资源时,锁的状态会如何变化呢?本篇文章来探讨一下。java
锁状态的分类git
Java 语言专门针对 synchronized
关键字设置了四种状态,它们分别是:无锁、偏向锁、轻量级锁和重量级锁,可是在了解这些锁以前还须要先了解一下 Java 对象头和 Monitor。github
咱们知道 synchronized 是悲观锁,在操做同步以前须要给资源加锁,这把锁就是对象头里面的,而Java 对象头又是什么呢?咱们以 Hotspot 虚拟机为例,Hopspot 对象头主要包括两部分数据:Mark Word(标记字段)
和 Klass Pointer(类型指针)
。安全
Mark Word:默认存储对象的HashCode,分代年龄和锁标志位信息。这些信息都是与对象自身定义无关的数据,因此Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽可能多的数据。它会根据对象的状态复用本身的存储空间,也就是说在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。性能优化
Klass Point:对象指向它的类元数据的指针,虚拟机经过这个指针来肯定这个对象是哪一个类的实例。网络
在32位虚拟机和64位虚拟机的 Mark Word 所占用的字节大小不同,32位虚拟机的 Mark Word 和 Klass Pointer 分别占用 32bits 的字节,而 64位虚拟机的 Mark Word 和 Klass Pointer 占用了64bits 的字节,下面咱们以 32位虚拟机为例,来看一下其 Mark Word 的字节具体是如何分配的数据结构
用中文翻译过来就是多线程
无锁
的时候,对象头开辟 25bit 的空间用来存储对象的 hashcode ,4bit 用于存放分代年龄,1bit 用来存放是否偏向锁的标识位,2bit 用来存放锁标识位为01偏向锁
中划分更细,仍是开辟25bit 的空间,其中23bit 用来存放线程ID,2bit 用来存放 epoch,4bit 存放分代年龄,1bit 存放是否偏向锁标识, 0表示无锁,1表示偏向锁,锁的标识位仍是01轻量级锁
中直接开辟 30bit 的空间存放指向栈中锁记录的指针,2bit 存放锁的标志位,其标志位为00重量级锁
中和轻量级锁同样,30bit 的空间用来存放指向重量级锁的指针,2bit 存放锁的标识位,为11GC标记
开辟30bit 的内存空间却没有占用,2bit 空间存放锁标志位为11。其中无锁和偏向锁的锁标志位都是01,只是在前面的1bit区分了这是无锁状态仍是偏向锁状态。oracle
关于为何这么分配的内存,咱们能够从 OpenJDK
中的 markOop.hpp 类中的枚举窥出端倪
来解释一下
synchronized
用的锁是存在Java对象头里的。
JVM基于进入和退出 Monitor 对象来实现方法同步和代码块同步。代码块同步是使用 monitorenter 和 monitorexit 指令实现的,monitorenter 指令是在编译后插入到同步代码块的开始位置,而 monitorexit 是插入到方法结束处和异常处。任何对象都有一个 monitor 与之关联,当且一个 monitor 被持有后,它将处于锁定状态。
根据虚拟机规范的要求,在执行 monitorenter 指令时,首先要去尝试获取对象的锁,若是这个对象没被锁定,或者当前线程已经拥有了那个对象的锁,把锁的计数器加1,相应地,在执行 monitorexit 指令时会将锁计数器减1,当计数器被减到0时,锁就释放了。若是获取对象锁失败了,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。
Synchronized是经过对象内部的一个叫作监视器锁(monitor)来实现的,监视器锁本质又是依赖于底层的操做系统的 Mutex Lock(互斥锁)来实现的。而操做系统实现线程之间的切换须要从用户态转换到核心态,这个成本很是高,状态之间的转换须要相对比较长的时间,这就是为何 Synchronized 效率低的缘由。所以,这种依赖于操做系统 Mutex Lock 所实现的锁咱们称之为重量级锁
。
Java SE 1.6为了减小得到锁和释放锁带来的性能消耗,引入了偏向锁
和轻量级锁
:锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。锁能够升级但不能降级。
因此锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁能够从偏向锁升级到轻量级锁,再升级的重量级锁(可是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。JDK 1.6中默认是开启偏向锁和轻量级锁的,咱们也能够经过-XX:-UseBiasedLocking=false来禁用偏向锁。
无锁状态
,无锁即没有对资源进行锁定,全部的线程均可以对同一个资源进行访问,可是只有一个线程可以成功修改资源。
无锁的特色就是在循环内进行修改操做,线程会不断的尝试修改共享资源,直到可以成功修改资源并退出,在此过程当中没有出现冲突的发生,这很像咱们在以前文章中介绍的 CAS 实现,CAS 的原理和应用就是无锁的实现。无锁没法全面代替有锁,但无锁在某些场合下的性能是很是高的。
Hotspot 的做者通过研究发现,大多数状况下,锁不只不存在多线程竞争,还存在锁由同一线程屡次得到的状况,偏向锁就是在这种状况下出现的,它的出现是为了解决只有在一个线程执行同步时提升性能。
能够从对象头的分配中看到,偏向锁要比无锁多了线程ID
和 epoch
,当一个线程访问同步代码块并获取锁时,会在对象头和栈帧的记录中存储线程的ID,等到下一次线程在进入和退出同步代码块时就不须要进行 CAS
操做进行加锁和解锁,只须要简单判断一下对象头的 Mark Word 中是否存储着指向当前线程的线程ID,判断的标志固然是根据锁的标志位来判断的。
访问 Mark Word 中偏向锁的标志是否设置成 1,锁的标志位是不是 01 --- 确认为可偏向状态。
若是确认为可偏向状态,判断当前线程id 和 对象头中存储的线程 ID 是否一致,若是一致的话,则执行步骤5,若是不一致,进入步骤3
若是当前线程ID 与对象头中存储的线程ID 不一致的话,则经过 CAS 操做来竞争获取锁。若是竞争成功,则将 Mark Word 中的线程ID 修改成当前线程ID,而后执行步骤5,若是不一致,则执行步骤4
若是 CAS 获取偏向锁失败,则表示有竞争(CAS 获取偏向锁失败则代表至少有其余线程曾经获取过偏向锁,由于线程不会主动释放偏向锁)。当到达全局安全点(SafePoint)时,会首先暂停拥有偏向锁的线程,而后检查持有偏向锁的线程是否存活(由于可能持有偏向锁的线程已经执行完毕,可是该线程并不会主动去释放偏向锁),若是线程不处于活动状态,则将对象头置为无锁状态(标志位为01)
,而后从新偏向新的线程;若是线程仍然活着,撤销偏向锁后升级到轻量级锁
的状态(标志位为00
),此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待得到该轻量级锁。
执行同步代码
偏向锁的释放过程能够参考上述的步骤4 ,偏向锁在遇到其余线程竞争锁时,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。偏向锁的撤销,须要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁是否处于被锁定状态,撤销偏向锁后恢复到未锁定(标志位为01
)或轻量级锁(标志位为00
)的状态。
偏向锁在Java 6 和Java 7 里是默认启用的。因为偏向锁是为了在只有一个线程执行同步块时提升性能,若是你肯定应用程序里全部的锁一般状况下处于竞争状态,能够经过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。
真正理解 epoch 的概念比较复杂,这里简单理解,就是 epoch 的值能够做为一种检测偏向锁有效性的时间戳
轻量级锁
是指当前锁是偏向锁的时候,被另外的线程所访问,那么偏向锁就会升级为轻量级锁
,其余线程会经过自旋的形式尝试获取锁,不会阻塞,从而提升性能。
在代码进入同步块的时候,若是同步对象锁状态为无锁状态(锁标志位为 01 状态,是否为偏向锁为 0 ),虚拟机首先将在当前线程的栈帧中创建一个名为锁记录(Lock Record)
的空间,用于存储锁对象目前的 Mark Word 的拷贝,而后拷贝对象头中的 Mark Word 复制到锁记录中。
拷贝成功后,虚拟机将使用 CAS 操做尝试将对象的 Mark Word 更新为指向 Lock Record 的指针,并将 Lock Record里的 owner 指针指向对象的 Mark Word。
若是这个更新动做成功了,那么这个线程就拥有了该对象的锁,而且对象Mark Word的锁标志位设置为 00 ,表示此对象处于轻量级锁定状态。
若是这个更新操做失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的栈帧,若是是就说明当前线程已经拥有了这个对象的锁,那就能够直接进入同步块继续执行。不然说明多个线程竞争锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为 10 ,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。
重量级锁也就是一般说 synchronized 的对象锁,锁标识位为10,其中指针指向的是 monitor 对象(也称为管程或监视器锁)的起始地址。每一个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如 monitor 能够与对象一块儿建立销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。
上图简单描述多线程获取锁的过程,当多个线程同时访问一段同步代码时,首先会进入 Entry Set当线程获取到对象的 monitor 后进入 The Owner 区域并把 monitor 中的 owner 变量设置为当前线程,同时 monitor 中的计数器count 加1,若线程调用 wait() 方法,将释放当前持有的 monitor,owner变量恢复为 null,count自减1,同时该线程进入 WaitSet 集合中等待被唤醒。若当前线程执行完毕也将释放 monitor (锁)并复位变量的值,以便其余线程进入获取monitor(锁)。
由此看来,monitor 对象存在于每一个Java对象的对象头中(存储的指针的指向),synchronized 锁即是经过这种方式获取锁的,也是为何Java中任意对象能够做为锁的缘由,同时也是 notify/notifyAll/wait 等方法存在于顶级对象Object中的缘由。(部分来源于网络)
下面为本身作个宣传,欢迎关注公众号 Java建设者,号主是Java技术栈,热爱技术,喜欢阅读,热衷于分享和总结,但愿能把每一篇好文章分享给成长道路上的你。关注公众号回复 002 领取为你特地准备的大礼包,你必定会喜欢并收藏的。
文章参考:
Synchronized锁性能优化偏向锁轻量级锁升级 多线程中篇(五)
citeseerx.ist.psu.edu/viewdoc/dow…
[java 偏向锁、轻量级锁及重量级锁synchronized原理](