前言

本篇文章介绍Java Synchronized锁优化。

锁是存在哪里的,怎么标识是什么锁

Monitor机制在Java中怎么表现的

锁优化

锁升级

1. 锁存在哪里

对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。

Hotspot虚拟机的对象头主要包括两部分数据:Mark Word(标记字段)、Klass Pointer(类型指针),数组会多1字宽(32位: 4字节)来存储数组长度。

synchronized用的锁是存在Java对象头里的。

其中Klass Point是对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例;Mark Word用于存储对象自身的运行时数据,它是实现轻量级锁和偏向锁的关键。

JVM中对象头的方式有以下两种(以32位JVM为例):

// 普通对象

|--------------------------------------------------------------|

| Object Header (64 bits) |

|------------------------------------|-------------------------|

| Mark Word (32 bits) | Klass Word (32 bits) |

|------------------------------------|-------------------------|

// 数组对象

|---------------------------------------------------------------------------------|

| Object Header (96 bits) |

|--------------------------------|-----------------------|------------------------|

| Mark Word(32bits) | Klass Word(32bits) | array length(32bits) |

|--------------------------------|-----------------------|------------------------|

Mark Word

这部分主要用来存储对象自身的运行时数据,如hashcode、gc分代年龄、锁状态标志、线程持有的锁、偏向线程 ID、偏向时间戳等等。

mark word的位长度为JVM的一个Word大小,也就是说32位JVM的Mark word为32位,64位JVM为64位。

Mark Word被设计成一个非固定的数据结构以便在极小的空间内存存储尽量多的数据,它会根据对象的状态复用自己的存储空间,为了让一个字大小存储更多的信息,JVM将字的最低两个位设置为标记位,不同标记位下的Mark Word示意如下:

|-------------------------------------------------------|--------------------|

| Mark Word (32 bits) | State |

|-------------------------------------------------------|--------------------|

| identity_hashcode:25 | age:4 | biased_lock:0 |lock:01 | Normal无锁 |

|-------------------------------------------------------|--------------------|

| thread:23 | epoch:2 | age:4 | biased_lock:1| lock:01 | Biased偏向锁 |

|-------------------------------------------------------|--------------------|

| ptr_to_lock_record:30 | lock:00 | Lightweight Locked轻量级锁 |

|-------------------------------------------------------|--------------------|

| ptr_to_heavyweight_monitor:30 | lock:10 | Heavyweight Locked重量级锁 |

|-------------------------------------------------------|--------------------|

| | lock:11 | Marked for GC GC标记|

|-------------------------------------------------------|--------------------|

锁状态

lock:2位的锁状态标记位,由于希望用尽可能少的二进制位表示尽可能多的信息,所以设置了lock标记。该标记的值不同,整个mark word表示的含义不同。

biased_lock:对象是否启用偏向锁标记,只占1个二进制位。为1时表示对象启用偏向锁,为0时表示对象没有偏向锁。

age:4位的Java对象年龄。在GC中,如果对象在Survivor区复制一次,年龄增加1。当对象达到设定的阈值时,将会晋升到老年代。默认情况下,并行GC的年龄阈值为15,并发GC的年龄阈值为6。由于age只有4位,所以最大值为15,这就是-XX:MaxTenuringThreshold选项最大值为15的原因。

identity_hashcode:25位的对象标识Hash码,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象被锁定时,该值会移动到管程Monitor中。

thread:持有偏向锁的线程ID。

epoch:偏向时间戳。

ptr_to_lock_record:指向栈中锁记录的指针。

ptr_to_heavyweight_monitor:指向管程Monitor的指针。

2. Monitor机制

Monitor其实是一种同步工具、同步机制,在Java中,Object 类本身就是监视者对象,Java 对于 Monitor Object 模式做了内建的支持,即每一个Java对象是天生的Monitor,每一个Java对象都有成为Monitor的潜质。

并且同时只能有一个线程可以获得该对象monitor的所有权。在线程进入时通过monitorenter尝试取得对象monitor所有权,退出时通过monitorexit释放对象monitor所有权。

monitorenter过程如下:

如果 monitor 的进入数为0,则该线程进入 monitor,然后将进入数设置为1,该线程即为 monitor 的所有者;

如果线程已经占有monitor,只是重新进入,则monitor的进入数+1;

如果其他线程已经占用 monitor,则该线程处于阻塞状态,直至 monitor 的进入数为0,再重新尝试获得 monitor 的所有权

monitorexit:

执行 monitorexit 的线程必须是 objectref 所对应的 monitor 的所有者。执行指令时,monitor 的进入数减1,如果减1后进入数为0,则线程退出 monitor,不再是这个 monitor 的所有者,其他被这个 monitor 阻塞的线程可以尝试获取这个 monitor 的所有权。

Monitor 是线程私有的数据结构,每一个线程都有一个可用monitor record列表,同时还有一个全局的可用列表。

每一个被锁住的对象都会和一个monitor关联(对象头的MarkWord中的LockWord指向monitor的起始地址),同时monitor中有一个Owner字段存放拥有该锁的线程的唯一标识,表示该锁被这个线程占用。

Monitor Record列表

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

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

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

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

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

Candidate:用来避免不必要的阻塞或等待线程唤醒,因为每一次只有一个线程能够成功拥有锁,如果每次前一个释放锁的线程唤醒所有正在阻塞或等待的线程,会引起不必要的上下文切换(从阻塞到就绪然后因为竞争锁失败又被阻塞)从而导致性能严重下降。Candidate只有两种可能的值,0表示没有需要唤醒的线程,1表示要唤醒一个继任线程来竞争锁。

3. 锁优化

jdk1.6对锁的实现引入了大量的优化,如自旋锁、适应性自旋锁、锁消除、锁粗化、偏向锁、轻量级锁等技术来减少锁操作的开销。

锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。

其中锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。

1. 重量级锁

monitor 监视器锁本质上是依赖操作系统的 Mutex Lock 互斥量 来实现的,我们一般称之为重量级锁。因为 OS 实现线程间的切换需要从用户态转换到核心态,这个转换过程成本较高,耗时相对较长,因此 synchronized 效率会比较低。

2. 轻量级锁

轻量级锁,其性能提升的依据是对于绝大部分的锁,在整个生命周期内都是不会存在竞争的,如果没有竞争,轻量级锁就可以使用 CAS 操作避免互斥量的开销,从而提升效率。

如果打破这个依据则除了互斥的开销外,还有额外的CAS操作,因此在有多线程竞争的情况下,轻量级锁比重量级锁更慢。

轻量级锁的加锁过程:

线程在进入到同步代码块的时候,JVM 会先在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象当前 Mark Word 的拷贝(官方称为 Displaced Mark Word),owner 指针指向对象的 Mark Word。此时堆栈与对象头的状态如图所示:

image.png

JVM 使用 CAS 操作尝试将对象头中的 Mark Word 更新为指向 Lock Record 的指针。如果更新成功,则执行步骤3;更新失败,则执行步骤4

如果更新成功,那么这个线程就拥有了该对象的锁,对象的 Mark Word 的锁状态为轻量级锁(标志位转变为'00')。此时线程堆栈与对象头的状态如图所示:

轻量级锁

如果更新失败,JVM 首先检查对象的 Mark Word 是否指向当前线程的栈帧

如果是,就说明当前线程已经拥有了该对象的锁,那就可以直接进入同步代码块继续执行

如果不是,就说明这个锁对象已经被其他的线程抢占了,当前线程会尝试自旋一定次数来获取锁。如果自旋一定次数 CAS 操作仍没有成功,那么轻量级锁就要升级为重量级锁(锁的标志位转变为'10'),Mark Word 中存储的就是指向重量级锁的指针,后面等待锁的线程也就进入阻塞状态

轻量级锁的解锁过程:

通过 CAS 操作用线程中复制的 Displaced Mark Word 中的数据替换对象当前的 Mark Word

如果替换成功,整个同步过程就完成了

如果替换失败,说明有其他线程尝试过获取该锁,那就在释放锁的同时,唤醒被挂起的线程

3. 偏向锁

依据:对于绝大部分锁,在整个同步周期内不仅不存在竞争,而且总由同一线程多次获得。

在一些情况下总是同一线程多次获得锁,此时第二次再重新做CAS修改对象头中的Mark Word这样的操作,有些多余。所以就有了偏向锁,只需要检查是否为偏向锁、锁标识为以及ThreadID即可,只要是同一线程就不再修改对象头。其目的为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径。

偏向锁枷锁过程:

检测Mark Word是否为可偏向状态,即是否为偏向锁1,锁标识位为01;

若为可偏向状态,则测试线程ID是否为当前线程ID,如果是,则执行步骤(5),否则执行步骤(3);

如果线程ID不为当前线程ID,则通过CAS操作竞争锁,竞争成功,则将Mark Word的线程ID替换为当前线程ID,否则执行线程(4);

通过CAS竞争锁失败,证明当前存在多线程竞争情况,当到达全局安全点,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块;

执行同步代码块

偏向锁释放过程:

当一个线程已经持有偏向锁,而另外一个线程尝试竞争偏向锁时,CAS 替换 ThreadID 操作失败,则开始撤销偏向锁。偏向锁的撤销,需要等待原持有偏向锁的线程到达全局安全点(在这个时间点上没有字节码正在执行),暂停该线程,并检查其状态

如果原持有偏向锁的线程不处于活动状态或已退出同步代码块,则该线程释放锁。将对象头设置为无锁状态(锁标志位为'01',是否偏向标志位为'0')

如果原持有偏向锁的线程未退出同步代码块,则升级为轻量级锁(锁标志位为'00')

4. 总结

锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,其升级如下图所示:

锁升级.png

优缺点

其他优化

自旋锁:

互斥同步时,挂起和恢复线程都需要切换到内核态完成,这对性能并发带来了不少的压力。同时在许多应用上,共享数据的锁定状态只会持续很短的一段时间,为了这段较短的时间而去挂起和恢复线程并不值得。那么如果有多个线程同时并行执行,可以让后面请求锁的线程通过自旋(CPU忙循环执行空指令)的方式稍等一会儿,看看持有锁的线程是否会很快的释放锁,这样就不需要放弃 CPU 的执行时间了。

适应性自旋:

自旋时如果锁被占用的时间比较短,那么自旋等待的效果就会比较好,而如果锁占用的时间很长,自旋的线程则会白白浪费 CPU 资源。解决这个问题的最简答的办法就是:指定自旋的次数,如果在限定次数内还没获取到锁(例如10次),就按传统的方式挂起线程进入阻塞状态。

JDK1.6 之后引入了自适应性自旋的方式,如果在同一锁对象上,一线程自旋等待刚刚成功获得锁,并且持有锁的线程正在运行中,那么 JVM 会认为这次自旋也有可能再次成功获得锁,进而允许自旋等待相对更长的时间(例如100次)。另一方面,如果某个锁自旋很少成功获得,那么以后要获得这个锁时将省略自旋过程,以避免浪费 CPU。

锁消除

虚拟机即时编译器(JIT)运行时,依据逃逸分析的数据检测到不可能存在竞争的锁,就自动将该锁消除)。锁消除的依据是逃逸分析的数据支持。

如果判断一段代码中,堆上的数据不会逃逸出去从而被其他线程访问到,则可以把他们当做栈上的数据对待,认为它们是线程私有的,不必要加锁。

如下所示,在 StringBuffer.append() 方法中有一个同步代码块,锁就是sb对象,但 sb 的所有引用不会逃逸到 concatString() 方法外部,其他线程无法访问它。因此这里有锁,但是在即时编译之后,会被安全的消除掉,忽略掉同步而直接执行了。

public String concatString(String s1, String s2, String s3) {

StringBuffer sb = new StringBuffer();

sb.append("a");

sb.append("b");

sb.append("c");

return sb.toString();

}

锁粗化

锁粗化就是 JVM 检测到一串零碎的操作都对同一个对象加锁,则会把加锁同步的范围粗化到整个操作序列的外部。

以上述 concatString() 方法为例,内部的 StringBuffer.append() 每次都会加锁,将会锁粗化,在第一次 append() 前至 最后一个 append() 后只需要加一次锁就可以了。

结语

本篇文章介绍了JVM对Synchronized进行的锁优化。自旋到自适应自旋、锁消除和锁粗化,从无锁到偏向锁、轻量级锁,都在避免线程进入内核态进行的切换。针对各种情况做的优化,

java 锁升级_Java并发 锁优化和锁升级相关推荐

  1. java lock 对象_Java并发编程锁系列之ReentrantLock对象总结

    Java并发编程锁系列之ReentrantLock对象总结 在Java并发编程中,根据不同维度来区分锁的话,锁可以分为十五种.ReentranckLock就是其中的多个分类. 本文主要内容:重入锁理解 ...

  2. java读写锁死锁例子_Java并发关于重入锁与读写锁的详解

    这篇文章主要介绍了Java并发编程之重入锁与读写锁,文中相关实例代码详细,测试可用,具有一定参考价值,需要的朋友可以了解下. 重入锁 重入锁,顾名思义,就是支持重进入的锁,它表示该锁能够支持一个线程对 ...

  3. java队列加锁_java并发-----浅析ReentrantLock加锁,解锁过程,公平锁非公平锁,AQS入门,CLH同步队列...

    前言 为什么需要去了解AQS,AQS,AbstractQueuedSynchronizer,即队列同步器.它是构建锁或者其他同步组件的基础框架(如ReentrantLock.ReentrantRead ...

  4. java split()方法_Java编程性能优化一些事儿

    点击上方 "程序员小乐"关注, 星标或置顶一起成长 每天凌晨00点00分, 第一时间与你相约 每日英文 Smile and stop complaining about the t ...

  5. java动态同步_java并发基础-Synchronized

    基础使用 基本上Java程序员都简单的了解synchronized的使用: 无非就是用在多线程环境下的同步. 看如下简单的例子: publicclassUnsafeCounter{ privatein ...

  6. java线程安全性_Java并发-线程安全性

    1.什么是线程安全性? 在线程安全性的定义中,最核心的就是正确性.当多线程访问调用某个类时,线程之间不会出现错误的交互,不管运行时线程如何交替执行,并且在主调代码不需要任何同步或协同,这个类都能表现出 ...

  7. java线程死锁_Java并发:隐藏线程死锁

    java线程死锁 大多数Java程序员熟悉Java线程死锁概念. 它本质上涉及2个线程,它们彼此永远等待. 这种情况通常是平面(同步)或ReentrantLock(读或写)锁排序问题的结果. Foun ...

  8. java社区活跃度_Java并发编程-活跃度问题

    在讲问题前,我先说明一下什么是活跃度? 一个并发应用及时执行的能力称作活跃度. 我主要讲死锁问题,顺带介绍一下饥饿,弱响应性和活锁. 死锁 死锁这个词大家都听过,我先来罗列一下产生死锁的四个必要条件: ...

  9. java 同步包_Java并发程序设计(四)JDK并发包之同步控制

    JDK并发包之同步控制 一.重入锁 重入锁使用java.util.concurrent.locks.ReentrantLock来实现.示例代码如下: public class TryReentrant ...

最新文章

  1. 小邪在阿里的十年技术人生
  2. 360浏览器打不开网页_苹果移动端、PC端safari浏览器打不开网页的解决方案!
  3. Biztalk Server 2006安装配置
  4. MongoDB数据库的删除与重建 - 精简篇
  5. 【.NET重修计划】数组,集合,堆栈的问题
  6. 2019 年 DevOps 实践中最有价值的8大技能
  7. Acrobat XI 安*装#教程*和安装%包分*享
  8. 天空机器人 联动存档_Wood R4 V1.38内核下载,自选存档大小,修正天空机器人美版等反烧录...
  9. 银行对公账户编码规则
  10. matlab高斯滤波去噪,图像平滑去噪之高斯滤波器
  11. “工资3000,一年存20万”:掌握理财技能,让我少奋斗10年
  12. 163个人电子邮箱如何注册申请,解密163个人邮箱如何注册?
  13. 【人情世故】汇报措辞
  14. iOS各版本发布时间汇总
  15. ICCV2019超分辨率方向论文整理笔记
  16. 帝国php被注入,帝国CMS(EmpireCMS) v7.5 代码注入分析(CVE-2018-19462)
  17. 最高效的七个云原生开发原则
  18. 基于回旋曲线的平行泊车路径规划
  19. Linux挂载命令mount 详解
  20. ai人工智能在手机的应用_何时更重要地在产品中利用人工智能

热门文章

  1. c语言数组从小到大冒泡排序,C语言冒泡排序将一个数组按照从小到大进行排序...
  2. 网站SEO优化诊断怎么做 分析诊断报告包含了哪些内容
  3. c语言双胞胎字符串个数,怀双胞胎的概率有多大 八十九个孕妇中会有一个
  4. nnDetection复现Luna16 附模型
  5. 微信里的apk链接打不开,怎么让微信的扫一扫扫描二维码后安装apk?
  6. 微信小程序----性别选择,并实现数据渲染
  7. ADI demo PL工程的编译-以adrv9371x_zc706为例子
  8. R语言在线地图神器:Leaflet for R包(三) 符号标记
  9. 互斥锁 、 自旋锁、读写锁和RCU锁
  10. python 正则表达式生成_python正则表达式