`

Java锁分析 - Synchronized执行流程

 
阅读更多

乐观锁 vs 悲观锁

乐观锁
 总是认为不会产生并发问题,因此并不会上锁,但是在更新时会判断其他线程在这之前有没有对数据进行修改,一般会使用版本号机制或CAS操作实现。
悲观锁
总是假设最坏的情况,每次取数据时都认为其他线程会修改,所以都会加锁(读锁、写锁等),当其他线程想要访问数据时,都需要阻塞挂起。

 

Java线程阻塞的代价

Java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统的接入,需要在用户态与内核态之间切换。这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间、寄存器等,用户态切换至内核态需要传递需要变量、参数给内核,内核也需要保护好用户态在切换时现场,包括一些寄存器的值、变量等,以便内核态调用结束后进行现场恢复。

 

对象头Mark Word

 HotSpot虚拟机中,对象在内存中存储的布局可以分为三个部分:对象头(Header)、实例数据(Instance Data)和对齐补充(Padding)。其中对象头(Header)包括两部分:Mark Word和类型指针。

 

Mark Word用于存储对象自身的运行时数据,如哈希码(HashCode)、GC分代年龄、锁状态标志、偏向线程ID等等,占用内存大小与虚拟机位长一致(32位或64位)。考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。下图是32位虚拟机中Mark Word在各个状态下存储的内容:



由此可知锁的状态保存在对象头中,是否偏向锁和锁标志位可以确定对象唯一的锁状态。
 

偏向锁、轻量级锁、自旋锁、重量级锁

 

上图中显示Java对象头中涉及到的4种锁,分别是偏向锁、轻量级锁、自旋锁和重量级锁。其中重量级锁属于悲观锁,而偏向锁、轻量级锁、自旋锁属于乐观锁。

 

1. 偏向锁

顾名思义,偏向锁会偏向于第一个占有锁的线程。如果没有竞争,已经获得偏向锁的线程,在将来进入同步块时不会进行同步操作。如果在运行过程中,其他线程也请求相同的锁时,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,升级为轻量级锁。

 

偏向锁消除了资源无竞争情况下的同步原语,可以提高程序的运行性能。但如果在竞争激烈的场合,偏向锁反而会增加系统负担,降低程序的运行性能。

 

偏向锁的获取

  1. 检查Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,确认为可偏向状态
  2. 如果为可偏向状态,则检查线程ID是否指向当前线程,如果是,则执行步骤5,否则执行步骤3
  3. 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程,然后执行步骤5;如果竞争失败,则执行步骤4
  4. 如果CAS获取偏向锁失败,则表示有竞争。当到达全局安全点(safepoint)时,获得偏向锁的线程被挂起(撤销偏向锁,会导致stop the world),偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码。
  5. 执行同步代码

偏向锁的释放

偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。如果没有竞争,线程不会主动释放偏向锁。偏向锁的撤销,需要等待全局安全点(没有字节码正在执行),JVM会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态,撤销偏向锁后恢复到未锁定(锁标志位为01)或轻量级锁(锁标志位为00)的状态。

 

偏向锁开启/关闭(默认启用)

  • 开启偏向锁:-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
  • 关闭偏向锁:-XX:-UseBiasedLocking

2. 轻量级锁

轻量级锁是由偏向所升级来的,偏向锁运行在一个线程进入同步块的情况下,当第二个线程加入锁争用的时候,偏向锁就会升级为轻量级锁。

 

轻量级锁的加锁过程

  • 在代码进入同步块的时候,如果同步对象锁状态为无锁状态(锁标志位为01,是否偏向锁为0),虚拟机首先将在当前线程的栈帧中创建一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为Displaced Mark Word。这时候线程堆栈与对象头的状态如下图



 

  • 将对象头的Mark Word复制到Lock Record中
  • 复制成功后,JVM将使用CAS操作尝试将对象头的Mark Word更新为指向Lock Record的指针,并将Lock Record里的owner指针指向Mark Word。如果更新成功,则执行步骤4,否则执行步骤5
  • 如果更新成功,则表示这个线程拥有了该对象的锁,并且对象Mark Word的锁标志位设置为00,即表示对象处于轻量级锁状态。此时线程堆栈与对象头的状态如下图



 

  • 如果更新失败,JVM首先会检查锁对象的Mark Word是否指向当前线程的栈帧,如果是则说明当前线程已经拥有了这个对象的锁,可以直接进入同步块继续执行。否则说明多个线程竞争锁,当前线程尝试使用自旋来获取锁。如果自旋获取锁成功,则依然处于轻量级锁状态,否则轻量级锁就要膨胀为重量级锁,锁标志位设置为10,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。

轻量级锁的释放 

  • 通过CAS操作将Lock Record中的Mark Word拷贝(Displaced Mark Word)替换锁对象的stack pointer(指向Lock Record的指针)
  • 如果操作成功,则同步完成
  • 如果失败,则说明已经有其他线程竞争当前对象锁。在当前线程持有锁时,如果其他线程竞争同一个锁,则竞争线程会修改对象头的Mark Word,将锁标志位设置为10,并更新为指向重量级锁的指针。当前线程(持有锁的线程)尝试自旋等待,如果自旋获取锁成功,则依然处于轻量级锁状态,否则轻量级锁就要膨胀为重量级锁,锁标志位设置为10,Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也要进入阻塞状态。

 

3. 自旋锁

自旋锁原理非常简单,如果持有锁的线程能在很短时间内释放锁资源,那么那些等待竞争锁的线程就不需要做内核态和用户态之间的切换进入阻塞挂起状态,它们只需要等一等(自旋),等持有锁的线程释放锁后即可立即获取锁,这样就避免用户线程和内核的切换的消耗。

 

但是线程自旋是需要消耗cup的,说白了就是让cup在做无用功,如果一直获取不到锁,那线程也不能一直占用cup自旋做无用功,所以需要设定一个自旋等待的最大时间。如果持有锁的线程执行的时间超过自旋等待的最大时间扔没有释放锁,就会导致其它争用锁的线程在最大等待时间内还是获取不到锁,这时线程会停止自旋进入阻塞状态。

 

JDK1.6引入了适应性自旋锁,适应性自旋锁意味着自旋的时间不在是固定的了,而是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定,基本认为一个线程上下文切换的时间是最佳的一个时间,同时JVM还针对当前CPU的负荷情况做了较多的优化。

  • 如果平均负载小于CPUs则一直自旋
  • 如果有超过(CPUs/2)个线程正在自旋,则后来线程直接阻塞
  • 如果正在自旋的线程发现Owner发生了变化则延迟自旋时间(自旋计数)或进入阻塞
  • 如果CPU处于节电模式则停止自旋
  • 自旋时间的最坏情况是CPU的存储延迟(CPU A存储了一个数据,到CPU B得知这个数据直接的时间差)
  • 自旋时会适当放弃线程优先级之间的差异

JDK1.7后默认启用,无需额外进行设置。

 

4. 重量级锁Synchronized

 Synchronized关键字用于保证同步,它可以把任意一个非NULL的对象当作锁。

  • 作用于实例方法时,锁住的是对象的实例(this)
  • 当作用于静态方法时,锁住的是Class实例

 

 小结

 

 Synchronized的执行流程:

  • 检查Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,确认为可偏向状态
  • 如果为可偏向状态,则检查线程ID是否指向当前线程,如果是则表示当前线程处于偏向锁状态,然后执行同步代码
  • 如果线程ID并未指向当前线程,则通过CAS操作竞争锁。如果竞争成功,则将Mark Word中线程ID设置为当前线程,偏向标志位设置为1,锁标志位设置为01,然后执行同步码块
  • 如果竞争失败,则说明发生竞争,撤销偏向锁,进而升级为轻量级锁
  • 当前线程使用CAS将对象头的Mark Word替换为锁记录指针,如果成功,当前线程获得锁
  • 如果替换失败,表示其他线程竞争锁,当前线程尝试自旋获取锁
  • 如果自旋成功,则依然处于轻量级锁状态
  • 如果自旋失败,则轻量级锁膨胀为重量级锁(monitor),后面等待锁的线程也要进入阻塞状态

 锁优化

减少锁的时间

不需要同步的代码,尽量不要放在同步块中执行,以便减少锁的持有时间,尽快释放锁。

 

减少锁的粒度

它的思想是将物理上的一个锁,拆成逻辑上的多个锁,增加并行度,从而降低锁的竞争。(空间换时间)

 

Java中很多数据结构都是采用这种方法提高并发操作的效率,比如ConcurrentHashMap中的segment数组(Segment继承自ReenTrantLock,所以每个Segment就是个可重入锁。put操作时先确定往哪个Segment放数据,只需要锁定这个Segment,其它的Segment不会被锁定)、LinkedBlockingQueue(LinkedBlockingQueue在队列头入队,在队列尾出队,入队和出队使用不同的锁)等。

 

锁粗化

大部分情况下我们是要让锁的粒度最小化,锁的粗化则是要增大锁的粒度。

在以下场景下需要粗化锁的粒度: 假如有一个循环,循环内的操作需要加锁,我们应该把锁放到循环外面,否则每次进出循环,都进出一次临界区,效率是非常差的。

 

锁分离

锁分离也是一种减小锁粒度的一种,这里强调对锁的功能进行分离,典型的就是读写锁ReentrantReadWriteLock,读操作加读锁,可以并发读;写操作使用写锁,不能并发写,而且写操作时无法获取读锁。读写锁可以在读多写少的系统中提高系统性能。

 

使用锁分离的数据结构有CopyOnWriteArrayList、 CopyOnWriteArraySet 等容器(即写时复制)、LinkedBlockingQueue等。

 

锁消除

在即时编译时,如果发现不可能被共享的对象加了锁(逃逸分析),则可以消除这些对象的锁操作。

 

无锁

锁是悲观操作,而无锁是乐观操作,在竞争不激烈的情况下,效率会比较高。无锁的一种实现方式是CAS(Compare And Swap)操作,默认不进行同步操作,在更新不成功的情况下重试。

 

参考

  1. 《深入JVM内核—原理、诊断与优化》系列课程 - 葛一鸣老师
  2. 【Java对象解析】不得不了解的对象头
  3.  java 中的锁 -- 偏向锁、轻量级锁、自旋锁、重量级锁
  • 大小: 68.4 KB
  • 大小: 39.9 KB
  • 大小: 53.2 KB
  • 大小: 78.5 KB
分享到:
评论

相关推荐

    Java面试宝典-经典

    54、简述synchronized和java.util.concurrent.locks.Lock的异同 ? 34 55、设计4个线程,其中两个线程每次对j增加1,另外两个线程对j每次减少1。写出程序。 36 56、子线程循环10次,接着主线程循环100,接着又回到...

    JAVA入门1.2.3:一个老鸟的JAVA学习心得 PART1(共3个)

    第4章 Java中的程序执行流程 67 教学视频:1小时57分钟 4.1 顺序执行 67 4.2 使用if-else让程序懂得判断 68 4.2.1 if语句 68 4.2.2 if语句的嵌套 71 4.2.3 if-else语句 73 4.2.4 if-else语句嵌套 75 4.3 ...

    Java入门1·2·3:一个老鸟的Java学习心得.PART3(共3个)

    第4章 Java中的程序执行流程 67 教学视频:1小时57分钟 4.1 顺序执行 67 4.2 使用if-else让程序懂得判断 68 4.2.1 if语句 68 4.2.2 if语句的嵌套 71 4.2.3 if-else语句 73 4.2.4 if-else语句嵌套 75 4.3 ...

    JAVA面试题最全集

    简述synchronized和java.util.concurrent.locks.Lock的异同 ? 34.EJB规范规定EJB中禁止的操作有哪些? 35.java除了8种基本类型外,在虚拟机里还有哪一种,有什么作用? 36.除了使用new关键字创建对象意外,试列举...

    Java JDK 7学习笔记(国内第一本Java 7,前期版本累计销量5万册)

    必须要时从Java SE API的源代码分析,了解各种语法在Java SE API中如何应用。  《Java JDK 7学习笔记》将IDE操作纳为教学内容之一,使读者能与实践结合,提供的视频教学能更清楚地帮助读者掌握操作步骤。 内容简介 ...

    Java常见面试题208道.docx

    面试题包括以下十九部分:Java 基础、容器、多线程、反射、对象拷贝、Java Web 模块、异常、网络、设计模式、Spring/Spring MVC、Spring Boot/Spring Cloud、Hibernate、Mybatis、RabbitMQ、Kafka、Zookeeper、MySql...

    Java进阶教程解密JVM视频教程

    JVM 是 Java 程序的运行环境,学习 JVM,方能了解 Java 程序是如何被执行的,为进一步深入底层原理乃至程序性能调优打好基础。通过学习这门课程,你将掌握:1. JVM 内存结构的组成、各部分功能作用,学会利用内存...

    java 面试题 总结

    JAVA相关基础知识 1、面向对象的特征有哪些方面 1.抽象: 抽象就是忽略一个主题中与当前目标无关的那些方面,以便更充分地注意与当前目标有关的方面。抽象并不打算了解全部问题,而只是选择其中的一部分,暂时不用...

    涵盖了90%以上的面试题

    重量级锁 synchronized 可重入锁 土方法实现可重入锁 使用AQS类实现可重入锁 CAS MySQL 中的行级锁、表级锁和页级锁 java中的死锁 公平锁和非公平锁 锁的总结 锁的优化 .......... 还有好多,不想写了,太多了,都是题...

    史上最全java面试,103项重点知识,带目录

    一、Java 基础 1 1. JDK 和 JRE 有什么区别? 1 2. == 和 equals 的区别是什么? 1 3. 两个对象的 hashCode()相同,则 equals()也一定为 true,对吗? 3 4. final 在 java 中有什么作用? 4 5. java 中的 Math.round...

    java面试题

    答:JDBC数据库连接,是一种用于执行SQL语句的Java API,可以为多种关系型数据库提供统一访问。 什么情况下不建议使用Hibernate? 答:当数据量大,并且表关系复杂的时候不建议使用。 sleep()和wait()有什么区别? ...

    Java面试宝典2010版

    54、简述synchronized和java.util.concurrent.locks.Lock的异同 ? 34 55、设计4个线程,其中两个线程每次对j增加1,另外两个线程对j每次减少1。写出程序。 36 56、子线程循环10次,接着主线程循环100,接着又回到...

    java面试题大全(2012版)

    54、简述synchronized和java.util.concurrent.locks.Lock的异同 ? 34 55、设计4个线程,其中两个线程每次对j增加1,另外两个线程对j每次减少1。写出程序。 36 56、子线程循环10次,接着主线程循环100,接着又回到...

    超级有影响力霸气的Java面试题大全文档

    超级有影响力的Java面试题大全文档 1.抽象: 抽象就是忽略一个主题中与当前目标无关的那些方面,以便更充分地注意与当前目标有关的方面。抽象并不打算了解全部问题,而只是选择其中的一部分,暂时不用部分细节。...

    最新Java面试宝典pdf版

    54、简述synchronized和java.util.concurrent.locks.Lock的异同 ? 34 55、设计4个线程,其中两个线程每次对j增加1,另外两个线程对j每次减少1。写出程序。 36 56、子线程循环10次,接着主线程循环100,接着又回到...

    java面试宝典2012

    54、简述synchronized和java.util.concurrent.locks.Lock的异同 ? 38 55、设计4个线程,其中两个线程每次对j增加1,另外两个线程对j每次减少1。写出程序。 40 56、子线程循环10次,接着主线程循环100,接着又回到...

Global site tag (gtag.js) - Google Analytics