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

相关文章

SpringBoot实现文件记录日志及日志文件自动归档和压缩

《SpringBoot实现文件记录日志及日志文件自动归档和压缩》Logback是Java日志框架,通过Logger收集日志并经Appender输出至控制台、文件等,SpringBoot配置logbac... 目录1、什么是Logback2、SpringBoot实现文件记录日志,日志文件自动归档和压缩2.1、

MQTT SpringBoot整合实战教程

《MQTTSpringBoot整合实战教程》:本文主要介绍MQTTSpringBoot整合实战教程,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考... 目录MQTT-SpringBoot创建简单 SpringBoot 项目导入必须依赖增加MQTT相关配置编写

spring-gateway filters添加自定义过滤器实现流程分析(可插拔)

《spring-gatewayfilters添加自定义过滤器实现流程分析(可插拔)》:本文主要介绍spring-gatewayfilters添加自定义过滤器实现流程分析(可插拔),本文通过实例图... 目录需求背景需求拆解设计流程及作用域逻辑处理代码逻辑需求背景公司要求,通过公司网络代理访问的请求需要做请

Spring Security介绍及配置实现代码

《SpringSecurity介绍及配置实现代码》SpringSecurity是一个功能强大的Java安全框架,它提供了全面的安全认证(Authentication)和授权(Authorizatio... 目录简介Spring Security配置配置实现代码简介Spring Security是一个功能强

SpringCloud使用Nacos 配置中心实现配置自动刷新功能使用

《SpringCloud使用Nacos配置中心实现配置自动刷新功能使用》SpringCloud项目中使用Nacos作为配置中心可以方便开发及运维人员随时查看配置信息,及配置共享,并且Nacos支持配... 目录前言一、Nacos中集中配置方式?二、使用步骤1.使用$Value 注解2.使用@Configur

Java 中的跨域问题解决方法

《Java中的跨域问题解决方法》跨域问题本质上是浏览器的一种安全机制,与Java本身无关,但Java后端开发者需要理解其来源以便正确解决,下面给大家介绍Java中的跨域问题解决方法,感兴趣的朋友一起... 目录1、Java 中跨域问题的来源1.1. 浏览器同源策略(Same-Origin Policy)1.

Java 关键字transient与注解@Transient的区别用途解析

《Java关键字transient与注解@Transient的区别用途解析》在Java中,transient是一个关键字,用于声明一个字段不会被序列化,这篇文章给大家介绍了Java关键字transi... 在Java中,transient 是一个关键字,用于声明一个字段不会被序列化。当一个对象被序列化时,被

在Java中基于Geotools对PostGIS数据库的空间查询实践教程

《在Java中基于Geotools对PostGIS数据库的空间查询实践教程》本文将深入探讨这一实践,从连接配置到复杂空间查询操作,包括点查询、区域范围查询以及空间关系判断等,全方位展示如何在Java环... 目录前言一、相关技术背景介绍1、评价对象AOI2、数据处理流程二、对AOI空间范围查询实践1、空间查

Java集成Onlyoffice的示例代码及场景分析

《Java集成Onlyoffice的示例代码及场景分析》:本文主要介绍Java集成Onlyoffice的示例代码及场景分析,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要... 需求场景:实现文档的在线编辑,团队协作总结:两个接口 + 前端页面 + 配置项接口1:一个接口,将o

javascript fetch 用法讲解

《javascriptfetch用法讲解》fetch是一个现代化的JavaScriptAPI,用于发送网络请求并获取资源,它是浏览器提供的全局方法,可以替代传统的XMLHttpRequest,这篇... 目录1. 基本语法1.1 语法1.2 示例:简单 GET 请求2. Response 对象3. 配置请求