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

相关文章

Java中流式并行操作parallelStream的原理和使用方法

《Java中流式并行操作parallelStream的原理和使用方法》本文详细介绍了Java中的并行流(parallelStream)的原理、正确使用方法以及在实际业务中的应用案例,并指出在使用并行流... 目录Java中流式并行操作parallelStream0. 问题的产生1. 什么是parallelS

Java中Redisson 的原理深度解析

《Java中Redisson的原理深度解析》Redisson是一个高性能的Redis客户端,它通过将Redis数据结构映射为Java对象和分布式对象,实现了在Java应用中方便地使用Redis,本文... 目录前言一、核心设计理念二、核心架构与通信层1. 基于 Netty 的异步非阻塞通信2. 编解码器三、

SpringBoot基于注解实现数据库字段回填的完整方案

《SpringBoot基于注解实现数据库字段回填的完整方案》这篇文章主要为大家详细介绍了SpringBoot如何基于注解实现数据库字段回填的相关方法,文中的示例代码讲解详细,感兴趣的小伙伴可以了解... 目录数据库表pom.XMLRelationFieldRelationFieldMapping基础的一些代

一篇文章彻底搞懂macOS如何决定java环境

《一篇文章彻底搞懂macOS如何决定java环境》MacOS作为一个功能强大的操作系统,为开发者提供了丰富的开发工具和框架,下面:本文主要介绍macOS如何决定java环境的相关资料,文中通过代码... 目录方法一:使用 which命令方法二:使用 Java_home工具(Apple 官方推荐)那问题来了,

Java HashMap的底层实现原理深度解析

《JavaHashMap的底层实现原理深度解析》HashMap基于数组+链表+红黑树结构,通过哈希算法和扩容机制优化性能,负载因子与树化阈值平衡效率,是Java开发必备的高效数据结构,本文给大家介绍... 目录一、概述:HashMap的宏观结构二、核心数据结构解析1. 数组(桶数组)2. 链表节点(Node

Java AOP面向切面编程的概念和实现方式

《JavaAOP面向切面编程的概念和实现方式》AOP是面向切面编程,通过动态代理将横切关注点(如日志、事务)与核心业务逻辑分离,提升代码复用性和可维护性,本文给大家介绍JavaAOP面向切面编程的概... 目录一、AOP 是什么?二、AOP 的核心概念与实现方式核心概念实现方式三、Spring AOP 的关

详解SpringBoot+Ehcache使用示例

《详解SpringBoot+Ehcache使用示例》本文介绍了SpringBoot中配置Ehcache、自定义get/set方式,并实际使用缓存的过程,文中通过示例代码介绍的非常详细,对大家的学习或者... 目录摘要概念内存与磁盘持久化存储:配置灵活性:编码示例引入依赖:配置ehcache.XML文件:配置

Java 虚拟线程的创建与使用深度解析

《Java虚拟线程的创建与使用深度解析》虚拟线程是Java19中以预览特性形式引入,Java21起正式发布的轻量级线程,本文给大家介绍Java虚拟线程的创建与使用,感兴趣的朋友一起看看吧... 目录一、虚拟线程简介1.1 什么是虚拟线程?1.2 为什么需要虚拟线程?二、虚拟线程与平台线程对比代码对比示例:三

Java中的.close()举例详解

《Java中的.close()举例详解》.close()方法只适用于通过window.open()打开的弹出窗口,对于浏览器的主窗口,如果没有得到用户允许是不能关闭的,:本文主要介绍Java中的.... 目录当你遇到以下三种情况时,一定要记得使用 .close():用法作用举例如何判断代码中的 input

Redis中Hash从使用过程到原理说明

《Redis中Hash从使用过程到原理说明》RedisHash结构用于存储字段-值对,适合对象数据,支持HSET、HGET等命令,采用ziplist或hashtable编码,通过渐进式rehash优化... 目录一、开篇:Hash就像超市的货架二、Hash的基本使用1. 常用命令示例2. Java操作示例三