返回顶部
首页 > 资讯 > 后端开发 > Python >Java synchronized最细讲解
  • 594
分享到

Java synchronized最细讲解

2024-04-02 19:04:59 594人浏览 独家记忆

Python 官方文档:入门教程 => 点击学习

摘要

目录前言Synchronization实现原理先理解Java对象头与Monitor1.对象头:锁的类型和状态和对象头的Mark Word息息相关;jdk6 之后做了改进,引入了偏向锁

前言

线程安全问题的主要诱因有两点,一是存在共享数据(也称临界资源),二是存在多条线程共同操作共享数据。

因此为了解决这个问题,我们可能需要这样一个方案,当存在多个线程操作共享数据时,需要保证同一时刻有且只有一个线程在操作共享数据,其他线程必须等到该线程处理完数据后再进行,这种方式有个高尚的名称叫互斥锁,即能达到互斥访问目的的锁,也就是说当一个共享数据被当前正在访问的线程加上互斥锁后,在同一个时刻,其他线程只能处于等待的状态,直到当前线程处理完毕释放该锁。

在 Java 中,关键字 synchronized可以保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块(主要是对方法或者代码块中存在共享数据的操作),同时我们还应该注意到synchronized另外一个重要的作用,synchronized可保证一个线程的变化(主要是共享数据的变化)被其他线程所看到(保证可见性,完全可以替代Volatile功能),这点确实也是很重要的。

synchronized三种作用范围(给对象加锁)

在静态方法上加锁;

在非静态方法上加锁;

在代码块上加锁;


public class SynchronizedSample {
 
    private final Object lock = new Object();
 
    private static int money = 0;
		//非静态方法
    public synchronized void noStaticMethod(){
        money++;
    }
		//静态方法
    public static synchronized void staticMethod(){
        money++;
    }
		
    public void codeBlock(){
      	//代码块
        synchronized (lock){
            money++;
        }
    }
}
作用范围 锁对象
非静态方法 当前对象 => this
静态方法 类对象 => SynchronizedSample.class (一切皆对象,这个是类对象)
代码块 指定对象 => lock (以上面的代码为例)

Synchronization实现原理

先理解Java对象头与Monitor

1.对象头:锁的类型和状态和对象头的Mark Word息息相关;

在这里插入图片描述

对象头分为二个部分,Mard WordKlass Word

对象头结构 存储信息-说明
Mard Word 存储对象的hashCode、锁信息或分代年龄或GC标志等信息
Klass Word 存储指向对象所属类(元数据)的指针,JVM通过这个确定这个对象属于哪个类

其中Mark Word在默认情况下存储着对象的HashCode、分代年龄、锁标记位等以下是32位JVM的Mark Word默认存储结构

锁状态 25bit 4bit 1bit是否是偏向锁 2bit 锁标志位
无锁状态 对象HashCode 对象分代年龄 0 01

在这里插入图片描述

主要分析一下重量级锁也就是通常说synchronized的对象锁,锁标识位为10,其中指针指向的是monitor对象(也称为管程或监视器锁)的起始地址。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如monitor可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。在Java虚拟机(HotSpot)中,monitor是由ObjectMonitor实现的,其主要数据结构如下(位于HotSpot虚拟机源码ObjectMonitor.hpp文件,c++实现的)


//👇图详细介绍重要变量的作用
ObjectMonitor() {
    _header       = NULL;
    _count        = 0;   // 重入次数
    _waiters      = 0,   // 等待线程数
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;  // 当前持有锁的线程
    _WaitSet      = NULL;  // 调用了 wait 方法的线程被阻塞 放置在这里
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; // 等待锁 处于block的线程 有资格成为候选资源的线程
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象),_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时,首先会进入 _EntryList 集合,当线程获取到对象的monitor 后进入 _Owner 区域并把monitor中的owner变量设置为当前线程同时monitor中的计数器count加1,若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。如下图所示

在这里插入图片描述

由此看来,monitor对象存在于每个Java对象的对象头中(存储的指针的指向),synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时也是notify/notifyAll/wait等方法存在于顶级对象Object中的原因(关于这点稍后还会进行分析)。

jdk6 之后做了改进,引入了偏向锁和轻量级锁:

  • 依赖底层操作系统的 mutex 相关指令实现,加锁解锁需要在用户态和内核态之间切换,性能损耗非常明显。
  • 研究人员发现,大多数对象的加锁和解锁都是在特定的线程中完成。也就是出现线程竞争锁的情况概率比较低。他们做了一个实验,找了一些典型的软件,测试同一个线程加锁解锁的重复率,如下图所示,可以看到重复加锁比例非常高。早期JVM 有 19% 的执行时间浪费在锁上。

1.无锁到偏向锁转化的过程

在这里插入图片描述

  • 首先A 线程访问同步代码块,使用CAS 操作将 Thread ID 放到 Mark Word 当中;
  • 如果CAS 成功,此时线程A 就获取了锁
  • 如果线程CAS 失败,证明有别的线程持有锁,例如上图的线程B 来CAS 就失败的,这个时候启动偏向锁撤销 (revoke bias);
  • 锁撤销流程:
    • 让 A线程在全局安全点阻塞(类似于GC前线程在安全点阻塞)
    • 遍历线程栈,查看是否有被锁对象的锁记录( Lock Record),如果有Lock Record,需要修复锁记录和Markword,使其变成无锁状态。
    • 恢复A线程
    • 将是否为偏向锁状态置为 0 ,开始进行轻量级加锁流程

2.偏向锁升级轻量级

  • 线程在自己的栈桢中创建锁记录 LockRecord。
  • 线程A 将 Mark Word 拷贝到线程栈的 Lock Record中
  • 将锁记录中的Owner指针指向加锁的对象(存放对象地址)
  • 将锁对象的对象头的MarkWord替换为指向锁记录的指针。
  • 这时锁标志位变成 00 ,表示轻量级锁

其实就是撤销偏向锁后,当前线程栈中会分配锁记录,并拷贝Mark Word到锁记录中。然后两个线程用CAS的方式去修改Mark Word中的指针指向自己,假如说第一个线程修改成功了,然后将锁升级为轻量级锁,去执行同步语句块中的内容。

3.轻量级到重量级

修改失败的第二个线程会进入自旋状态,自旋结束后会继续去尝试CAS修改指针指向自己。如果自旋失败超过一定次数的时候(这个次数会动态进行调整),会请求JVM将此时的锁状态升级为重量级锁,这是依赖于底层操作系统的调度库来实现的。接着将Mark Word指向重量级锁Monitor的指针,然后挂起当前第二个线程(被放在Monitor的_EntryList中)。等一个线程执行完毕后,会查看当前Mark Word中的指针是否仍然指向自己,如果是自己的话就释放锁,否则不是自己的话,说明此时已经升级成了重量级锁,除了释放锁之后,还会唤醒阻塞的线程,进行新一轮的锁竞争。在此之后,该锁就一直会是重量级锁存在了

ps:为什么设计自旋数超过一定限制设置为重量级锁?

一般来说,同步代码块内的代码应该很快就执行结束,这时候修改失败的第二个线程自旋一段时间是很容易拿到锁的,但是如果不巧,没拿到,自旋其实就是死循环,很耗CPU的,因此就直接转成重量级锁咯,这样就不用了线程一直自旋了。

源码才学疏浅只了解到:

synchronized 在代码块上是通过 monitorenter 和 monitorexit指令实现,在静态方法和 方法上加锁是在方法的flags 中加入 ACC_SYNCHRONIZED 。JVM 运行方法时检查方法的flags,遇到同步标识开始启动前面的加锁流程,在方法内部遇到monitorenter指令开始加锁。

总结

本篇文章就到这里了,希望能够给你带来帮助,也希望您能够多多关注编程网的更多内容!

--结束END--

本文标题: Java synchronized最细讲解

本文链接: https://lsjlt.com/news/135270.html(转载时请注明来源链接)

有问题或投稿请发送至: 邮箱/279061341@qq.com    QQ/279061341

猜你喜欢
  • Java synchronized最细讲解
    目录前言Synchronization实现原理先理解Java对象头与Monitor1.对象头:锁的类型和状态和对象头的Mark Word息息相关;jdk6 之后做了改进,引入了偏向锁...
    99+
    2024-04-02
  • Java 枚举详细讲解
    目录 什么是枚举? 如何使用Java枚举? 如何使用Java枚举中的常量值? 如何在Java枚举中添加方法? 什么是枚举? 枚举是一种特殊的数据类型,用于定义具有固定个数的常量集。它可以帮助我们更好地管理常量,使代码更易于阅读和维护。 ...
    99+
    2023-09-01
    java 开发语言 javase 面向对象 枚举
  • java反射超详细讲解
    目录Java反射超详解✌1.反射基础1.1Class类1.2类加载2.反射的使用2.1Class对象的获取2.2Constructor类及其用法2.4Method类及其用...
    99+
    2024-04-02
  • 超详细讲解Java异常
    目录一、Java异常架构与异常关键字Java异常简介Java异常架构1、Throwable2、Error(错误)3、Exception(异常)4、受检异常与非受检异常Java异常关键...
    99+
    2024-04-02
  • Java的Spring AOP详细讲解
    目录什么是AOP&作用AOP的动态代理技术基于JDK的动态代理cglib动态代理AOP相关概念AOP开发明确事项需要编写的内容AOP技术实现的内容AOP 底层使用哪种代理方式...
    99+
    2024-04-02
  • Java详细分析讲解HashMap
    目录1.HashMap数据结构2.HashMap特点3.HashMap中put方法流程java集合容器类分为Collection和Map两大类,Collection类的子接口有Set...
    99+
    2024-04-02
  • Java split方法详细讲解
    1. 问题描述 描述:在日常编写代码时,我们经常遇到需要将一串字符串中的数据进行分析摘取,从中获得分隔符外的数据,此时便不得不提split方法。 2. 方法介绍 分隔符可以是任意字符、符号、数字、字符串等。 2.1 split(String...
    99+
    2023-09-10
    java 开发语言
  • Java的Stream流详细讲解
    一.Stream 是什么 Stream是Java 8新增的重要特性, 它提供函数式编程支持并允许以管道方式操作集合. 流操作会遍历数据源, 使用管道式操作处理数据后生成结果集合, 这个过程通常不会对数据源造成影响。 ​ 同时stream不是...
    99+
    2023-08-31
    java 开发语言
  • Java 抽象类详细讲解
    目录 Java抽象类概念 Java抽象类示例 继承Animal类的子类的示例 Java抽象类详细使用方法 1、定义抽象类 2、继承抽象类 3、实现抽象方法 4、完整示例代码 Java抽象类概念 Java中抽象类是指用abstract关键...
    99+
    2023-09-04
    java jvm 开发语言 javase 面向对象
  • Java异常Exception详细讲解
    目录1、异常中最大的父类Throwable2、try-catch-finally三条语句注意的问题3、final-finally-finalize的各作用4、throws关键字5、t...
    99+
    2024-04-02
  • Java@GlobalLock注解详细分析讲解
    目录GlobalLock的作用全局锁为什么要使用GlobalLock工作原理GlobalLock的作用 对于某条数据进行更新操作,如果全局事务正在进行,当某个本地事务需要更新该数据时...
    99+
    2022-11-21
    Java @GlobalLock Java @GlobalLock注解
  • 超详细讲解Java线程池
    目录池化技术池化思想介绍池化技术的应用如何设计一个线程池Java线程池解析ThreadPoolExecutor使用介绍内置线程池使用ThreadPoolExecutor解析整体设计线...
    99+
    2024-04-02
  • Java超详细讲解hashCode方法
    目录1、介绍一下hashCode方法2、为什么需要hashCode方法?3、hashCode(),equals()两种方法是什么关系?4、为什么重写 equals 方法必须重...
    99+
    2024-04-02
  • Java 超详细讲解字符流
    目录一、字符流的由来二、编码表字符集:Unicode字符集:UTF-8编码规则:三、字符串中的编码解码问题编码方法(IDEA):解码方法(IDEA):四、字符流的编码解码问题四、字符...
    99+
    2024-04-02
  • Java详细分析讲解泛型
    目录1.泛型概念2.泛型的使用2.1泛型类语法2.2泛型方法语法2.3泛型接口语法2.4泛型在main方法中的使用3.擦除机制4.泛型的上界5.通配符5.1通配符的上界5.2通配符的...
    99+
    2024-04-02
  • Java超详细透彻讲解static
    目录1. 引入2. 理解3. 使用3.1 使用范围3.2 static修饰属性3.2.1 设计思想3.2.2 分类3.2.3 注意3.2.4 举例3.2.5 类变量内存解析3.3 s...
    99+
    2024-04-02
  • Java中ThreadPoolExecutor类的详细讲解
    这篇文章主要介绍“Java中ThreadPoolExecutor类的详细讲解”,在日常操作中,相信很多人在Java中ThreadPoolExecutor类的详细讲解问题上存在疑惑,小编查阅了各式资料,整理出简单好用的操作方法,希望对大家解答...
    99+
    2023-06-20
  • Java窗口精细全方位讲解
    目录一、新建简单窗口二、编写窗口中的按键三、简单的按键运行1.流布局管理器: 2.静态文本框: 四、窗口画图五、窗口鼠标响应六、总结 好了,stop! 我们呢 咳咳咳 下面呢 ...
    99+
    2024-04-02
  • Java 泛型超详细入门讲解
    目录1、什么是泛型2、泛型是怎么编译的泛型的编译机制:擦除机制1、什么是泛型 泛型其实就是将类型作为参数传递,泛型允许程序员在编写代码时使用一些以后才指定的类型 ,在实例化该类时将想...
    99+
    2024-04-02
  • Java 超详细讲解SpringMVC拦截器
    目录拦截器(interceptor)的作用拦截器和过滤器区别拦截器快速入门多拦截器操作拦截器方法说明本章小结拦截器(interceptor)的作用 Spring MVC 的 拦截器 ...
    99+
    2024-04-02
软考高级职称资格查询
编程网,编程工程师的家园,是目前国内优秀的开源技术社区之一,形成了由开源软件库、代码分享、资讯、协作翻译、讨论区和博客等几大频道内容,为IT开发者提供了一个发现、使用、并交流开源技术的平台。
  • 官方手机版

  • 微信公众号

  • 商务合作