阅读本文至少要知道 synchronized 用来是干什么的... 需要的前置知识还有 Java 对象头和 Java 字节码的部分知识。

synchronized 的使用

synchronized 有三种使用方式,三种方式锁住的对象是不相同的。

锁分为实例对象锁class 对象锁类对象锁,注意这三种锁是不一样的。

  • 修饰实例方法,此时锁住的是对象,锁分为实例对象锁
  • 修饰静态方法,此时锁住的是类对象锁
  • 修饰代码段,此时锁住的是括号中的对象(synchronized(this)),可以是实例对象锁或者 class 对象锁(synchronized(Object.class)

此时出现了锁住类和锁住对象,要注意这两个锁是不同的,在一个线程拿到类的锁时,另外一个线程是可以拿到对象的锁的。

synchronized 底层语义实现

每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如 monitor 可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。

当多个线程同时请求某个对象监视器时,新请求锁的线程将首先被加入到 ConetentionList 中。对象监视器会设置几种状态用来区分请求的线程:

Contention List:所有请求锁的线程将被首先放置到该竞争队列

Entry List:Contention List 中那些有资格成为候选人的线程被移到Entry List

Wait Set:那些调用 wait 方法被阻塞的线程被放置到 Wait Set

OnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为 OnDeck

Owner:获得锁的线程称为 Owner

!Owner:释放锁的线程

代码同步块和方法级别的 synchronized 使用在JVM 层实现是不一样的。

synchrionized 在代码同步块的入口插入 monitorenter,在同步块出口插入monitorexit 来实现互斥,可以通过反编译看到。

方法级别的同步是隐式的,在字节码层面上没有显示出来。JVM 可以从方法常量池中的方法表结构中的 ACC_SYNCHRONIZED 字段访问标志区分一个方法是否为同步方法。如果是同步方法,则去获取 monitor,然后执行方法。
具体可以查看《深入理解 JVM 虚拟机》 中字节码一章。

对 synchronized 的优化

JDK 1.6 实现了对锁的大量优化。可以分为两种,一种是减少对 synchronized 的使用,一种是在特殊条件下使用更轻量级的锁来代替 synchronized。

减少对锁的使用

锁消除

当编译器检测到一些被加上 synchronized 的代码不存在竞争的时候(通过逃逸分析,感兴趣可以去看一下《深入理解 Java 虚拟机》),就会被视为线程私有的,锁会被安全的消除掉。

锁粗化

当编译器发现 synchronized 被加入在循环当中,不断的加锁解锁会有极大的效率问题。不要认为你不会写出这么傻的代码,JDK 中有许多方法是同步的,比如 HashTable 中的一些方法。

for (int i = 0; i < 100; i++) {synchronized (this) {//do something}
}

编译器会自动把它优化成

synchronized (this) {for (int i = 0; i < 100; i++) {//do something}
}

来减少锁的获取和释放。

自旋锁与自适应锁

有相当多一段代码在代码同步块中只运行一小会儿,如果为了等待这一会儿去挂起和恢复线程,切换线程带来的开销不是很值得,在引入了自旋锁后,当遇到锁被别的线程占用的时候,这个线程就进入一段忙循环,这就是自旋。

但是如果多次忙循环后仍然获取不到锁,那么只能挂起线程将锁升级为重量级锁了。

自适应锁会记录之前在代码同步快的运行时间来决定是否要执行自旋以及自旋的时间,如果之前自旋成功过,那么这次也很有可能会自旋成功。如果之前自旋失败,那么就省略掉自旋过程直接挂起线程避免浪费 CPU 资源。

通过轻量级锁来代替 synchronized

轻量级锁设计出来是想要在竞争较少的情况下减少 synchronized 的性能消耗,而不是用来代替 synchronized 的。想要看懂轻量级锁的使用需要对 Java 对象头有一定的了解。关于 Java 对象头可以参考。好,接下来我就默认认为你懂 Mark Word 是什么了。

锁的膨胀过程是 偏向锁→轻量级锁→重量级锁,膨胀过程的单方向的。不能缩小回来。

下面是 Mark Word 的内容和锁的关系。

存储内容 标志位 状态
对象哈希码,对象分代年龄 01 未锁定
指向记录锁指针 00 轻量级锁定
指向重量级锁指针 10 膨胀(重量级锁定)
11 GC 标记
偏向线程 id,时间戳,分代年龄 01 可偏向

偏向锁

偏向锁的思想就是:锁经常被同一个线程重复获取,那么可以通过设置偏向锁来避免使用重量级锁。因为如果这段时间只有这一个线程在重复获取这个对象的锁,那么对这部分代码的同步就是无意义的。

当线程获取锁的时候发现 Mark Word 是未锁定的状态,那么就采用 CAS 把这个 Mark Word 设置成偏向状态,把这个线程的 id 设置进去,然后如果这个线程再次获取这个锁的时候发现这个偏向锁的 id 和当前线程的 id 一样则不需要同步直接运行。

当有另外一个线程尝试获取这个偏向锁的时候,锁会恢复到未锁定或者轻量级锁的状态。

  • 如果对象未被锁定,则会变成未锁定的,不可偏向的对象
  • 如果对象被锁定了,则会变成轻量级锁状态

如果大多数锁总是被多个不同的线程访问,那么偏向模式就是多余的,可以 采用 --XX:UseBiaseLocking 来禁止偏向锁来提高性能。

轻量级锁

当线程进入一个代码同步块的时候,虚拟机将使用 CAS 将 Mark Word 更新为指向 Lock Record 的指针。如果成功则线程拥有这个对象锁,mark word 将被设为 00。

如果更新失败,则检查该线程是否持有这个对象锁,如果已经持有则直接向下执行

如果没有持有这个对象锁则轻量级锁膨胀为重量级锁,锁标志状态变为 10。

参考文献

  • 周志明. 深入理解 Java 虚拟机 [M]. 机械工业出版社, 2011.
  • 方腾飞.Java 并发编程的艺术 [M]. 机械工业出版社, 2015.
  • 深入理解Java并发之synchronized实现原理
  • [JVM底层又是如何实现synchronized的

转载于:https://www.cnblogs.com/zjmeow/p/9818739.html

自顶向下彻底理解 Java 中的 Synchronized相关推荐

  1. 自顶向下彻底理解 Java 中的 volatile 关键字

    标题 neta 自<计算机网络自顶向下> 思维导图 volatile 在 Java 中被称为轻量级 synchronized.很多并发专家引导用户远离 volatile 变量,因为使用它们 ...

  2. 如何理解 JAVA 中的 volatile 关键字

    如何理解 JAVA 中的 volatile 关键字 最近在重新梳理多线程,同步相关的知识点.关于 volatile 关键字阅读了好多博客文章,发现质量高适合小白的不多,最终找到一篇英文的非常通俗易懂. ...

  3. 深入理解Java中的逃逸分析

    转载自  深入理解Java中的逃逸分析 在Java的编译体系中,一个Java的源代码文件变成计算机可执行的机器指令的过程中,需要经过两段编译,第一段是把.java文件转换成.class文件.第二段编译 ...

  4. 一文带你理解Java中Lock的实现原理

    转载自   一文带你理解Java中Lock的实现原理 当多个线程需要访问某个公共资源的时候,我们知道需要通过加锁来保证资源的访问不会出问题.java提供了两种方式来加锁,一种是关键字:synchron ...

  5. java byreference_深入理解Java中的引用(一)——Reference

    深入理解Java中的引用(一)--Reference 本系列文章首先会介绍Reference类,为之后介绍的强引用.软引用.弱引用和虚引用打下基础. 最后会介绍虚引用在DirectBuffer回收中的 ...

  6. 深入理解Java中的String(原地址https://www.cnblogs.com/xiaoxi/p/6036701.html)

    深入理解Java中的String 一.String类 想要了解一个类,最好的办法就是看这个类的实现源代码,来看一下String类的源码: public final class Stringimplem ...

  7. 深入理解Java并发之synchronized实现原理

    [版权申明]未经博主同意,谢绝转载!(请尊重原创,博主保留追究权) http://blog.csdn.net/javazejian/article/details/72828483 出自[zejian ...

  8. 深入理解Java中的内存泄漏

    理解Java中的内存泄漏,我们首先要清楚Java中的内存区域分配问题和内存回收的问题本文将分为三大部分介绍这些内容. Java中的内存分配 Java中的内存区域主要分为线程共享的和线程私有的两大区域: ...

  9. 理解Java中的弱引用(Weak Reference)

    理解Java中的弱引用(Weak Reference) 本篇文章尝试从What.Why.How这三个角度来探索Java中的弱引用,理解Java中弱引用的定义.基本使用场景和使用方法.由于个人水平有限, ...

最新文章

  1. linux启动spark命令,在linux上安装spark
  2. webpack从入门到精通(二)开发环境的基本配置
  3. 如何绘制计算机软件程序流程图?
  4. ios设置中性黑体_ios 解决自定义字体无法显示问题
  5. Pdshell教程-利用现有数据库(没有PDM情况下)导出数据库PMD文件
  6. 谈谈ASP.NET Core中的ResponseCaching
  7. 玄学小记.5 ~ Bluestein's algorithm
  8. 收藏 | YOLOX模型部署、优化及训练全过程
  9. shell读取php 数组长度,shell数组的定义、数组长度
  10. Zabbix监控和分布式部署实施方案
  11. Python量化交易学习笔记(39)——BaoStock股票数据下载
  12. 音乐播放小程序demo
  13. HTML学生个人网站作业设计——HTML+CSS+JavaScript简单的大学生书店网页制作(13页) web期末作业设计网页 web结课作业的源码 web网页设计实例作业
  14. cpu怎么开启php,win10开启cpu虚拟化的方法
  15. 结对作业 ——UI组第八组 冯富禹 齐天浩
  16. Web应用中设置欢迎页面
  17. bmi计算 python_《Python之BMI计算》
  18. Hbase慢请求常规排查流程
  19. (干货)Adobe软件分享
  20. RHEL-Linux安全加固与基础优化(一)

热门文章

  1. 如何在 Windows 7 中建立逻辑分区
  2. DataGrid中页导航栏的自定义样式
  3. nitrous.io mysql_云IDE:Nitrous.io的介绍以及活用手段
  4. 双目测距测深度_TOF还能这么玩?荣耀V20黑科技升级变测距神器
  5. java list 不包含_java判断list是否包含某个值
  6. python xml解析dom_如何解析python中表示xml.dom.minidom节点的字符串?
  7. java对docker_Java和Docker限制问题
  8. idea代码区分成两屏显示
  9. 038_Unicode对照表四
  10. 008_Redis的ZSet数据类型