Java synchronized 原理

2024-09-04 10:44
文章标签 java 原理 synchronized

本文主要是介绍Java synchronized 原理,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

Synchronized使用

synchronized关键字可使用在方法上或代码块上表示一段同步代码块:

public class SyncTest {public void syncBlock(){synchronized (this){System.out.println("hello block");}}public synchronized void syncMethod(){System.out.println("hello method");}
}

当在方法上指定synchronized时,编译后的字节码会在方法的flag上标记ACC_SYNCHRONIZED

在代码块上指定synchronized时,编译后的字节码会使用monitorentermonitorexit包裹代码块,通常包含一个monitorenter和两个monitorexit,有两个monitorexit指令的原因是:为了保证抛异常的情况下也能释放锁,所以javac为同步代码块添加了一个隐式的try-finally,在finally中会调用monitorexit命令释放锁。

上面Java代码编译后的字节码如下:

{public void syncBlock();descriptor: ()Vflags: ACC_PUBLICCode:stack=2, locals=3, args_size=10: aload_01: dup2: astore_13: monitorenter				 	  // monitorenter指令进入同步块4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;7: ldc           #3                  // String hello block9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V12: aload_113: monitorexit						  // monitorexit指令退出同步块14: goto          2217: astore_218: aload_119: monitorexit						  // monitorexit指令退出同步块20: aload_221: athrow22: returnException table:from    to  target type4    14    17   any17    20    17   anypublic synchronized void syncMethod();descriptor: ()Vflags: ACC_PUBLIC, ACC_SYNCHRONIZED      //添加了ACC_SYNCHRONIZED标记Code:stack=2, locals=1, args_size=10: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;3: ldc           #5                  // String hello method5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V8: return}

锁类型和对象头

锁类型

本文基于JDK 1.8。

锁类型可分为:

  • 偏向锁
  • 轻量级锁
  • 重量级锁

偏向锁和轻量级锁在JDK 1.6引入:为了解决在没有多线程竞争或基本没有竞争的场景下因使用传统锁机制带来的性能开销问题。

对象头

对象的组成有3个部分:

  • 对象头
  • 实例数据
  • 对齐填充字节: 保证对象大小是8byte的整数倍

其中对象头包含3个部分:

  • Mark Word: 存储hashcode、年龄、锁类型等信息,32位机器上占4字节,64位占8字节
  • Klass Point: 指向元空间中类元信息的指针,开启指针压缩占4字节,关闭占8字节
  • 数组长度(只有数组有)

其中Mark Word在32位和64位的组成分别如下图:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

我们引入以下依赖实践一下:

<!--查看对象头工具-->
<dependency><groupId>org.openjdk.jol</groupId><artifactId>jol-core</artifactId><version>0.16</version>
</dependency>
import org.openjdk.jol.info.ClassLayout;public class Test {static class World {}static class Hello {boolean bool;boolean bool2;Boolean bool3;String string;boolean bool5;Integer integer;int i;World world = new World();}public static void main(String[] args) {System.out.println(ClassLayout.parseInstance(new Hello()).toPrintable());}
}

在64位机器上运行以上代码输出:

OFF  SZ                TYPE DESCRIPTION               VALUE0   8                     (object header: mark)     0x0000000000000001 (non-biasable; age: 0)8   4                     (object header: class)    0x00060a1812   4                 int Hello.i                   016   1             boolean Hello.bool                false17   1             boolean Hello.bool2               false18   1             boolean Hello.bool5               false19   1                     (alignment/padding gap)   20   4   java.lang.Boolean Hello.bool3               null24   4    java.lang.String Hello.string              null28   4   java.lang.Integer Hello.integer             null32   4          Test.World Hello.world               (object)36   4                     (object alignment gap)    
Instance size: 40 bytes

可以看到对象头占用12个字节,实例数据占用24字节,对其填充占用4字节,共40个字节。(boolean占1个字节,但会padding到4字节)。

再看下数组:

Hello[] hellos = {new Hello(), new Hello(), new Hello()};
System.out.println(ClassLayout.parseInstance(hellos).toPrintable());

在64位机器上运行以上代码输出:

OFF  SZ         TYPE DESCRIPTION                VALUE0   8              (object header: mark)      0x0000000000000001 (non-biasable; age: 0)8   4              (object header: class)     0x00060c1012   4              (array length)             312   4              (alignment/padding gap)    16  12   Test$Hello [LTest$Hello;.<elements>   N/A28   4              (object alignment gap)     
Instance size: 32 bytes

可以看到对象头加了4个字节00 00 00 03表示数组长度3,如果数组长度超过4个字节表示的范围会发生什么?会编译不通过,只能接受int类型做数组长度。

锁升级

偏向锁

当JVM启用了偏向锁模式(-XX:-UseBiasedLocking 1.6以上默认开启),当新建一个锁对象,
如果该对象所属的class没有关闭偏向锁模式(什么时候会关闭一个class的偏向模式下文会说,默认所有class的偏向模式都是是开启的),
则该对象的Mark Word标记为将是偏向锁状态, 此时Mark Word中的线程id为0,表示未偏向任何线程,也叫做匿名偏向(anonymously biased)。

下图展示了锁状态的转换流程:
外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

加锁过程
  1. 当该对象第一次被线程获得锁的时候,发现是匿名偏向状态,则会用CAS指令,将Mark Word中的线程id由0改成当前线程Id。如果成功,则代表获得了偏向锁,继续执行同步块中的代码。否则,将偏向锁撤销,升级为轻量级锁。

  2. 当被偏向的线程再次进入同步块时,发现锁对象偏向的就是当前线程,在通过一些额外的检查后,会往当前线程的栈中添加一条Displaced Mark Word为null的Lock Record,然后继续执行同步块的代码,因为操纵的是线程私有的栈,因此不需要用到CAS指令;由此可见偏向锁模式下,当被偏向的线程再次尝试获得锁时,仅仅进行几个简单的操作就可以了,在这种情况下,synchronized关键字带来的性能开销基本可以忽略。

  3. 当其他线程进入同步块时,发现已经有偏向的线程了,则会进入到撤销偏向锁的逻辑里,一般来说,会在safe point中去查看偏向的线程是否还存活,如果存活且还在同步块中则将锁升级为轻量级锁,原偏向的线程继续拥有锁,当前线程则走入到锁升级的逻辑里;如果偏向的线程已经不存活或者不在同步块中,则将对象头的Mark Word改为无锁状态(unlocked),之后再升级为轻量级锁。

由此可见,偏向锁升级的时机为:当锁已经发生偏向后,只要有另一个线程尝试获得偏向锁,则该偏向锁就会升级成轻量级锁。当然这个说法不绝对,因为还有批量重偏向这一机制。

HotSpot JVM在第一次调用Object.hashCodeSystem.identityHashCode时计算身份hashcode,并将其存储在对象头中。随后的调用只是从头中提取以前计算的值。如果hashcode已经存到了对象头,则偏向锁无效,当该锁对象处于非偏向状态其他线程进入同步代码块会直接上轻量级锁,当处于偏向状态时计算hashcode也要将偏向锁失效并升级为重量级锁。

解锁过程

当有其他线程尝试获得锁时,是根据遍历偏向线程的Lock Record来确定该线程是否还在执行同步块中的代码。因此偏向锁的解锁很简单,仅仅将栈中的最近一条Lock Record_obj字段设置为null。
需要注意的是,偏向锁的解锁步骤中并不会修改对象头中的线程id。

关于Lock Record的结构如下:

class BasicObjectLock {...private:BasicLock _lock; // 锁, must be double word alignedoop       _obj;  // 锁对象指针
};class BasicLock {private:volatile markOop _displaced_header; // 对象头里的mark word
};

另外,偏向锁默认不是立即就启动的,在程序启动后,通常有几秒的延迟,可以通过命令 -XX:BiasedLockingStartupDelay=0来关闭延迟。

轻量级锁

当存在多个线程访问一个同步代码块时,偏向锁会升级为轻量级锁。

线程在执行同步块之前,JVM会先在当前的线程的栈帧中创建一个Lock Record,其包括一个用于存储对象头中的 Mark Word(官方称之为Displaced Mark Word)以及一个指向锁对象的指针。如下图所示:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

加锁过程
  1. 在线程栈中创建一个Lock Record,将其_obj(即上图的Object reference)字段指向锁对象。

  2. 直接通过CAS指令将Lock Record的地址存储在对象头的Mark Word中,如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。如果失败,进入到步骤3。

  3. 如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分(Displaced Mark Word)为null,起到了一个重入计数器的作用。然后结束。

  4. 走到这一步说明发生了竞争,需要膨胀为重量级锁。

解锁过程
  1. 遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record。

  2. 如果Lock RecordDisplaced Mark Word为null,代表这是一次重入,将_obj设置为null后continue。

  3. 如果Lock RecordDisplaced Mark Word不为null,则利用CAS指令将对象头的Mark Word恢复成为Displaced Mark Word。如果成功,则continue,否则膨胀为重量级锁。

重量级锁

当线程CAS抢轻量级锁自旋10次失败后,则升级为重量级锁。

重量级锁的状态下,对象的Mark Word为指向一个堆中monitor对象的指针。

一个monitor对象包括这么几个关键字段:cxqEntryListWaitSetowner

其中cxqEntryListWaitSet都是由ObjectWaiter的链表结构,owner指向持有锁的线程。

当一个线程尝试获得锁时,如果该锁已经被占用,则会将该线程封装成一个ObjectWaiter对象插入到cxq的队列尾部,然后暂停当前线程。当持有锁的线程释放锁前,会将cxq中的所有元素移动到EntryList中去,并唤醒EntryList的队首线程。

如果一个线程在同步块中调用了Object#wait方法,会将该线程对应的ObjectWaiterEntryList移除并加入到WaitSet中,然后释放锁。当wait的线程被notify之后,会将对应的ObjectWaiterWaitSet移动到EntryList中。

参考:

  • 死磕Synchronized底层实现
  • 深入浅出偏向锁

这篇关于Java synchronized 原理的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1135804

相关文章

Spring Boot中WebSocket常用使用方法详解

《SpringBoot中WebSocket常用使用方法详解》本文从WebSocket的基础概念出发,详细介绍了SpringBoot集成WebSocket的步骤,并重点讲解了常用的使用方法,包括简单消... 目录一、WebSocket基础概念1.1 什么是WebSocket1.2 WebSocket与HTTP

SpringBoot+Docker+Graylog 如何让错误自动报警

《SpringBoot+Docker+Graylog如何让错误自动报警》SpringBoot默认使用SLF4J与Logback,支持多日志级别和配置方式,可输出到控制台、文件及远程服务器,集成ELK... 目录01 Spring Boot 默认日志框架解析02 Spring Boot 日志级别详解03 Sp

java中反射Reflection的4个作用详解

《java中反射Reflection的4个作用详解》反射Reflection是Java等编程语言中的一个重要特性,它允许程序在运行时进行自我检查和对内部成员(如字段、方法、类等)的操作,本文将详细介绍... 目录作用1、在运行时判断任意一个对象所属的类作用2、在运行时构造任意一个类的对象作用3、在运行时判断

java如何解压zip压缩包

《java如何解压zip压缩包》:本文主要介绍java如何解压zip压缩包问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录Java解压zip压缩包实例代码结果如下总结java解压zip压缩包坐在旁边的小伙伴问我怎么用 java 将服务器上的压缩文件解压出来,

SpringBoot中SM2公钥加密、私钥解密的实现示例详解

《SpringBoot中SM2公钥加密、私钥解密的实现示例详解》本文介绍了如何在SpringBoot项目中实现SM2公钥加密和私钥解密的功能,通过使用Hutool库和BouncyCastle依赖,简化... 目录一、前言1、加密信息(示例)2、加密结果(示例)二、实现代码1、yml文件配置2、创建SM2工具

Spring WebFlux 与 WebClient 使用指南及最佳实践

《SpringWebFlux与WebClient使用指南及最佳实践》WebClient是SpringWebFlux模块提供的非阻塞、响应式HTTP客户端,基于ProjectReactor实现,... 目录Spring WebFlux 与 WebClient 使用指南1. WebClient 概述2. 核心依

Spring Boot @RestControllerAdvice全局异常处理最佳实践

《SpringBoot@RestControllerAdvice全局异常处理最佳实践》本文详解SpringBoot中通过@RestControllerAdvice实现全局异常处理,强调代码复用、统... 目录前言一、为什么要使用全局异常处理?二、核心注解解析1. @RestControllerAdvice2

Spring IoC 容器的使用详解(最新整理)

《SpringIoC容器的使用详解(最新整理)》文章介绍了Spring框架中的应用分层思想与IoC容器原理,通过分层解耦业务逻辑、数据访问等模块,IoC容器利用@Component注解管理Bean... 目录1. 应用分层2. IoC 的介绍3. IoC 容器的使用3.1. bean 的存储3.2. 方法注

Spring事务传播机制最佳实践

《Spring事务传播机制最佳实践》Spring的事务传播机制为我们提供了优雅的解决方案,本文将带您深入理解这一机制,掌握不同场景下的最佳实践,感兴趣的朋友一起看看吧... 目录1. 什么是事务传播行为2. Spring支持的七种事务传播行为2.1 REQUIRED(默认)2.2 SUPPORTS2

怎样通过分析GC日志来定位Java进程的内存问题

《怎样通过分析GC日志来定位Java进程的内存问题》:本文主要介绍怎样通过分析GC日志来定位Java进程的内存问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录一、GC 日志基础配置1. 启用详细 GC 日志2. 不同收集器的日志格式二、关键指标与分析维度1.