synchronized分析

该文章是本人学习记录总结的,有误请指出,感谢。java

一开始学习Java时,介绍Java的同步机制那就必然是synchronized。但以后又了解到synchronized是一个重量级锁,因此应当尽可能使用Lock。编程

以后又了解到Java1.6对synchronized进行了优化。数组

因此除非:安全

  • 业务须要获取锁能够被中断
  • 须要获取锁能够超时
  • 能够尝试着获取锁

的状况下使用Lock,应当尽可能使用synchronized。代码更加简洁。并发

1、synchronized介绍

由于是Java语法提供的,也能够称为内置锁。ide

根据做为锁的对象不一样,可分为性能

  • 对象锁
    • 声明在非静态方法上时,以当前类的实例做为锁。
    • 同步代码块中,以括号里的对象做为锁。
  • 类锁。
    • 声明在静态方法上时则以当前类的字节码对象做为锁也就是类锁。

2、synchronized实现

从上图能够看出synchronized代码块经过monitorenter和monitorexit指令实现,由JVM保证monitorenter保证有一个配对的monitorexit。学习

synchronized方法则没有特别的指令,而是在Class文件的方法表中将该方法的access_flags字段中的synchronized标志位置1,表示该方法是同步方法并使用调用该方法的对象或该方法所属的Class在JVM的内部对象表示Klass作为锁对象。测试

3、Java对象头和Monitor

先要了解一下Java对象头和monitor

一、对象头

对象头由三部分组成

1.一、Mark Word

Mark Word为一个字大小,即在32位JVM长度为32位,在64位JVM长度为64位。

由于Mark Word用于存储与对象自定义数据无关的数据,为了节省空间,会根据对象的状态不一样存放不一样的数据。

32位JVM存储格式:

状态(State) 25bit 4bit 1bit 2bit
23bit 2bit 是否偏向锁(biased_lock):1bit 锁标志位(lock):2bit
无锁(normal) 对象的散列值(identity_hashcode) 分代年龄(age) 0 01
偏向锁(Biased) 线程ID(threadID) 偏向时间戳(epoch) 分代年龄(age) 1 01
轻量级锁(Lightweight Locked) 指向栈中记录的指针(ptr_to_lock_record) 00
重量级锁(Heavyweight Locked) 指向管程的指针(ptr_to_heavyweight_monitor) 10
GC标记(Marked for GC) 空(null) 11

JDK1.6以后存在锁升级的概念,JVM对同步锁的处理随着竞争激烈,处理方式从偏向锁到轻量级锁再到重量级锁。

1.二、klass pointer

用于存储对象的类型指针,该指针指向它的元数据,大小为一个字。

1.三、array length(数组对象才有)

只有数组对象才有这部分数据

由于JVM虚拟机能够经过Java对象的元数据信息肯定Java对象的大小,可是没法从数组的元数据来确认数组的大小,因此用一块来记录数组长度。

参考:

Java对象头详解

Java的对象头和对象组成详解

二、Monitor Record

Monitor Record是线程私有的,每一个线程都有一个Monitor Record列表,同时还有一个全局可用列表。每个做为锁的对象都会与一个Monitor Record关联(对象头的MarkWord中的LockWord指向monitor record的起始地址)。

Owner
EntryQ
RcThis
Nest
HashCode
Candidate

Owner:初始时为NULL表示当前没有任何线程拥有该monitor record,当线程成功拥有该锁后保存线程惟一标识,当锁被释放时又设置为NULL;

EntryQ:关联一个系统互斥锁(semaphore),阻塞全部试图锁住monitor record失败的线程。

RcThis:表示blocked或waiting在该monitor record上的全部线程的个数。

Nest:用来实现重入锁的计数。

HashCode:保存从对象头拷贝过来的HashCode值(可能还包含GC age)。

Candidate:用来避免没必要要的阻塞或等待线程唤醒,由于每一次只有一个线程可以成功拥有锁,若是每次前一个释放锁的线程唤醒全部正在阻塞或等待的线程,会引发没必要要的上下文切换(从阻塞到就绪而后由于竞争锁失败又被阻塞)从而致使性能严重降低。Candidate只有两种可能的值0表示没有须要唤醒的线程,1表示要唤醒一个继任线程来竞争锁。

4、synchronized的优化

锁粗化(Lock Coarsening)

也就是减小没必要要的紧连在一块儿的unlock,lock操做,将多个连续的锁扩展成一个范围更大的锁。

锁消除(Lock Elimination)

经过运行时JIT编译器的逃逸分析来消除一些没有在当前同步块之外被其余线程共享的数据的锁保护,经过逃逸分析也能够在线程本地Stack上进行对象空间的分配(同时还能够减小Heap上的垃圾收集开销)。

适应性自旋(Adaptive Spinning)

所谓自适应就意味着自旋的次数再也不是固定的,它是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。线程若是自旋成功了,那么下次自旋的次数会更加多,由于虚拟机认为既然上次成功了,那么这次自旋也颇有可能会再次成功,那么它就会容许自旋等待持续的次数更多。反之,若是对于某个锁,不多有自旋可以成功的,那么在之后要或者这个锁的时候自旋的次数会减小甚至省略掉自旋过程,以避免浪费处理器资源。

偏向锁(Biased Locking)和轻量级锁(Lightweight Locking)

一开始我对于锁的了解就是拿到了就执行任务,拿不到就阻塞。

Java的线程时是映射到操做系统原生线程上的,线程的阻塞和唤醒都须要操做系统的介入,须要在用户态和核心态之间转换当,这种切换会消耗掉大量的系统资源(由于用户态和系统态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态须要传递给许多变量、参数给内核,内核也须要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工做 摘自:Java线程阻塞的代价)。

所以,JVM使用锁会逐步升级:无锁->偏向锁->轻量级锁->重量级锁

锁只能升级,不能降级

  1. ​ 初始没有线程使用锁,Mark Word为无锁状态

  2. 偏向锁:
    加锁
    • 测试Mark Word中是否指向当前线程,是的话表示当前线程已获取该锁,不是则判断是不是偏向锁。
    • 不是偏向锁则会CAS操做在对象头存储当前线程ID,之后该线程在进入和退出同步块时不须要花费CAS操做来加锁和解锁,而只需简单的测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。
    • 如果偏向锁则表示当前锁有其它线程在使用,存在竞争,偏向锁会膨胀为轻量级锁。
    膨胀
    • 当前线程CAS获取锁失败,撤销偏向锁(不是释放的意思,偏向锁没有释放):发现锁存在竞争,

    等待全局安全点(此时间点,全部的工做线程都停了字节码的执行),经过ID找到已得到偏向锁的线程,挂起该线程,从该线程的Monitor Record列表得到一个空闲记录,并将锁对象的对象头设为轻量级锁状态,将Lock Record更新为指向该空闲记录的指针。到这里锁撤销完成,被挂起的线程继续运行。

    • 撤销完成后,对象可能处于两种状态,不可偏向的无锁状态和不可偏向的已锁状态。
      • 不可偏向的无锁状态:本来获取偏向锁的线程执行完了同步块。
      • 不可偏向的已锁状态:本来获取偏向锁的线程未执行完了同步块。此时对象应该被转换为轻量级加锁的状态。
    批量再偏向:

    ​ 偏向锁这个机制很特殊, 别的锁在执行完同步代码块后, 都会有释放锁的操做, 而偏向锁并无直观意义上的“释放锁”操做。

    那么做为开发人员, 很天然会产生的一个问题就是, 若是一个对象先偏向于某个线程, 执行完同步代码后, 另外一个线程就不能直接从新得到偏向锁吗? 答案是能够, JVM 提供了批量再偏向机制(Bulk Rebias)机制

    该机制的主要工做原理以下:

    • 引入一个概念 epoch, 其本质是一个时间戳 , 表明了偏向锁的有效性
    • 从前文描述的对象头结构中能够看到, epoch 存储在可偏向对象的 MarkWord 中。
    • 除了对象中的 epoch, 对象所属的类 class 信息中, 也会保存一个 epoch 值
    • 每当遇到一个全局安全点时, 若是要对 class C 进行批量再偏向, 则首先对 class C 中保存的 epoch 进行增长操做, 获得一个新的 epoch_new
    • 而后扫描全部持有 class C 实例的线程栈, 根据线程栈的信息判断出该线程是否锁定了该对象, 仅将 epoch_new 的值赋给被锁定的对象中。
    • 退出安全点后, 当有线程须要尝试获取偏向锁时, 直接检查 class C 中存储的 epoch 值是否与目标对象中存储的 epoch 值相等, 若是不相等, 则说明该对象的偏向锁已经无效了, 能够尝试对此对象从新进行偏向操做。
  3. 轻量级锁:
    加锁:
    1. 经过对象判断锁对象对象头是不是无锁状态,是则JVM首先将在当前线程的栈帧中创建一个名为锁记录(Lock Record)的空间,用于存储所对象的Mark Word的拷贝(官方把这份拷贝加了一个Displaced前缀,即Displaced Mark Word),拷贝成功后,尝试CAS操做将Mark Word的Lock Record更新为指向moniter record的指针。若更新失败,则表示竞争很激烈,须要膨胀为重量级锁。
    2. 锁对象处于不可偏向无锁状态,多个线程CAS操做试图将Mark Word更新为指向本身的Monitor Word的指针,更新成功的获取到锁,失败的线程进入状况4。
    3. 锁对象处于不可偏向的已锁状态,同时Mark Word中是指向本身的Monitor Word,这就是重入(reentrant)锁的状况,只须要简单的将Nest加1便可。不须要任何原子操做,效率很是高。
    4. 锁对象处于不可偏向的已锁状态,同时Mark Word中不是指向本身的Monitor Word,线程自旋必定次数仍然获取失败后膨胀。
    释放锁:
    1. 首先检查该对象是否处于膨胀状态而且该线程是这个锁的拥有者,若是发现不对则抛出异常;
    2. 检查Nest字段是否大于1,若是大于1则简单的将Nest减1并继续拥有锁,若是等于1,则进入到第3步;
    3. 检查rfThis是否大于0,设置Owner为NULL而后唤醒一个正在阻塞或等待的线程再一次试图获取锁,若是等于0则进入到第4步
    4. 缩小(deflate)一个对象,经过将对象的LockWord置换回原来的HashCode值来解除和monitor record之间的关联来释放锁,同时将monitor record放回到线程是有的可用monitor record列表。
  4. 重量级锁:

    重量级锁依赖于操做系统的互斥量(mutex) 实现。

  5. 小结

    偏向锁、轻量级锁、重量级锁适用于不一样的并发场景:

    • 偏向锁:无实际竞争,且未来只有第一个申请锁的线程会使用锁。
    • 轻量级锁:无实际竞争,多个线程交替使用锁;容许短期的锁竞争。
    • 重量级锁:有实际竞争,且锁竞争时间长。

    另外,若是锁竞争时间短,可使用自旋锁进一步优化轻量级锁、重量级锁的性能,减小线程切换。
    若是锁竞争程度逐渐提升(缓慢),那么从偏向锁逐步膨胀到重量锁,可以提升系统的总体性能。

参考:

Java中的偏向锁,轻量级锁, 重量级锁解析

Java中synchronized的实现原理与应用

Java的对象头和对象组成详解

【java并发编程实战4】偏向锁-轻量锁-重量锁的那点秘密(synchronize实现原理)

整篇参考:

JVM内部细节之一:synchronized关键字及实现细节(轻量级锁Lightweight Locking)

【死磕Java并发】—–深刻分析synchronized的实现原理

相关文章
相关标签/搜索