多线程知识梳理(3) synchronized 三部曲之锁优化

1、前言

多线程知识梳理(2) - synchronized 基本使用 中,咱们介绍了使用重量锁来实现的synchronized。今天,咱们就来一块儿学习一下在JDK 1.6以后,对synchronized所采起的一系列优化措施。html

2、对象头 & Monitor Record

在介绍优化方法以前,咱们须要介绍两个重要的概念Java对象头和Monitor编程

2.1 对象头

Java&Android 基础知识梳理(3) - 内存区域 中介绍内存区域的时候,对于一个Java对象所占的内存区域是这么介绍的: 安全

在运行过程当中,对象头所包含数据的含义不是固定不变的,随着 锁状态标志位(下图中红框的范围)的改变,其它字段所表示的含义也不一样,以 32位的虚拟机为例,下图就是锁状态标志位所对应的数据结构含义:

2.2 Monitor Record

Monitor是线程私有的数据结构,因为一个线程可能进入多个不一样的同步方法,这些方法有可能会关联到不一样的Monitor,所以每个线程都有一个可用的Monitor列表,同时还有一个全局的可用列表,Monitor数据结构包括如下成员变量:数据结构

  • Owner:初始时为空表示当前没有任何线程拥有该Monitor,当线程成功拥有该锁后保存线程惟一标识,当锁被释放时又设置为空。
  • EntryQ:关联一个系统互斥锁,阻塞全部试图得到Monitor可是最终失败了的线程。
  • RcThis:表示blockedwaiting在该Monitor上的全部线程的个数。
  • Nest:用来实现重入锁的计数。
  • HashCode:保存从对象头拷贝过来的HashCode值。
  • Candidate:用来避免没必要要的阻塞或等待线程唤醒,由于每一次只有一个线程可以成功拥有锁,若是每次前一个释放锁的线程唤醒全部正在阻塞或等待的线程,会引发没必要要的上下文切换(从阻塞到就绪而后由于竞争锁失败又被阻塞)从而致使性能严重降低。Candidate只有两种可能的值:0表示没有须要唤醒的线程,1表示要唤醒一个继任线程来竞争锁。

3、实现优化

JDK 1.6以后,它对于锁进行了一系列的优化措施,主要包括:自适应自旋锁、锁消除和锁粗化。多线程

3.1 自旋锁

因为线程的阻塞和唤醒须要CPU从用户态转换成核心态,而频繁的阻塞和唤醒对CPU来讲是一件负担很重的工做。并发

所以,咱们在发现锁已经被其它线程占有时,并不直接让当前线程进入阻塞状态,而是让线程执行一段无心义的循环,待循环结束后,如何仍然没法获取到锁,那么才进入阻塞状态。性能

决定自旋锁性能的关键在于自旋次数的选择,在JDK 1.6以后,引入了自适应自旋锁,它会根据前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定新的自旋次数。学习

3.2 锁消除

JVM检测到不可能存在共享数据竞争,会对同步锁进行锁消除。优化

3.3 锁粗化

在使用同步锁的时候,须要让同步块的做用范围尽量地小,仅在共享数据的实际做用域中才进行同步,这样作的目的是为了使须要同步的操做数量尽量缩小,若是存在锁竞争,那么等待锁的线程也能尽快拿到锁。操作系统

然而,若是一系列连续加锁解锁操做,可能会致使没必要要的性能损耗,因此有时能够将多个连续的加锁、解锁操做链接在一块儿,扩展成一个范围更大的锁。

4、状态优化

JDK 1.6以前,锁只有两种状态:无锁状态和重量级锁状态,而在这以后增长为四种状态:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,这种改进基于两点考虑:

  • 无锁状态和重量级锁状态之间的切换是依赖于底层操做系统的Mutex Lock实现,操做系统实现线程之间的切换须要从用户态切换到内核态,切换成本很高。
  • 实验研究发现,对于绝大部分的锁,在整个生命周期内都是不存在竞争的。

须要注意,对于锁的这四种状态,它们会随着竞争的激烈而逐渐升级,可是它只容许锁升级,不容许锁降级。

无锁状态和重量级锁状态都比较好理解,下面咱们主要介绍新增的两种锁状态:偏向锁状态轻量级锁状态

整个转换的流程图以下所示,在后面的介绍中能够参考:

4.1 偏向锁状态

引入偏向锁的目的是:在无多线程竞争的状况下,尽可能减小没必要要的轻量级锁执行路径,它的理想状况下是在无竞争时把整个同步都去掉,连CAS操做都省略。

偏向锁的意思是这个锁会偏向于第一个得到它的线程,若是在接下来的执行过程当中,该锁没有被其它线程获取,则持有偏向锁的线程将永远不须要再进行同步。

4.1.1 获取偏向锁

(a) 前提条件

获取偏向锁的前提条件是synchronized所修饰的对象处于可偏向状态

  • 锁状态为01
  • 偏向锁状态为1

(b) 获取过程

当知足前提条件时,再去判断对象的Mark Word中的线程ID是否指向当前线程

  • 若是不指向当前线程,那么经过CAS操做竞争锁
    • 竞争成功:将Mark Word的线程ID替换为当前线程ID,接着执行同步代码块
    • 竞争失败:证实存在多线程竞争的状况,当到达全局安全点,得到偏向锁的线程被挂起,偏向锁升级为轻量级锁,而后被阻塞在安全点的线程继续往下执行同步代码块
  • 若是指向当前线程,那么执行同步代码块

4.1.2 释放偏向锁

(a) 前提条件

释放偏向锁的前提条件是其它的线程在竞争偏向锁的过程当中出现了失败的状况,而且偏向锁的释放须要等待到达全局安全点。

(b) 释放过程

当知足释放偏向锁的前提条件时,首先会暂停拥有偏向锁的线程,接着判断锁对象是否处于被锁定的状态,决定锁标志位下一步的状态:

  • 若是未被锁定,那么将锁标志至为01,偏向锁状态置为0,表示它处于无锁,且不可偏向状态。
  • 若是已经被锁定,那么将锁标志置为00,表示它处于被轻量级锁定的状态。

4.2 轻量级锁状态

引入轻量级锁的目的是:在无多线程竞争的状况下,减小传统的重量级锁使用操做系统互斥量产生的性能消耗。

4.2.1 获取轻量级锁

(a) 前提条件

获取轻量级锁的前提条件时当前对象处于无锁状态,

  • 锁状态标志位为01
  • 偏向锁标志位为0

(b) 获取过程

JVM首先将在当前线程的栈帧中创建一个名为锁记录(Lock Record)的空间,用于存储对象目前的Mark Word的拷贝,以后JVM利用CAS操做尝试将对象的Mark Word更新为指向Lock Record的指针:

  • 操做成功:将锁标志置为00,表示处于锁定的状态,以后执行同步操做。
  • 操做失败:那么检查对象的Mark Word是否指向当前线程的栈针
  • 若是是,则直接执行同步代码块
  • 若是不是,说明该锁对象已经被其余线程抢占了,此时轻量级锁升级为重量锁,锁标志位变为10,后面等待的线程将会进入阻塞状态。

4.2.2 释放轻量级锁

(a) 释放过程

轻量级锁的释放也是经过CAS操做来进行的:

  • 取出在获取轻量级锁时,保存在Displaced Mark Word中的数据。
  • CAS操做将取出的数据替换到当前对象的Mark Word中:
  • 若是成功,则说明释放锁成功
  • 若是失败,说明有其它线程尝试获取该锁,那么须要在释放锁的同时,唤醒须要被唤醒的线程

对于轻量级锁,它性能提高的依据是默认"对于绝大部分的锁,在整个生命周期内是不会存在竞争的",若是不符合这种状况,那么除了互斥的开销外,还有额外的CAS操做,这样轻量级锁比重量级锁更慢。

5、参考文章

Java 并发编程:Synchronized 底层优化(偏向锁、轻量级锁) 死磕 Java 并发 -----深刻分析 synchronized 的实现原理

相关文章
相关标签/搜索