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

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

深入探索协同过滤:从原理到推荐模块案例

文章目录 前言一、协同过滤1. 基于用户的协同过滤(UserCF)2. 基于物品的协同过滤(ItemCF)3. 相似度计算方法 二、相似度计算方法1. 欧氏距离2. 皮尔逊相关系数3. 杰卡德相似系数4. 余弦相似度 三、推荐模块案例1.基于文章的协同过滤推荐功能2.基于用户的协同过滤推荐功能 前言     在信息过载的时代,推荐系统成为连接用户与内容的桥梁。本文聚焦于

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听