Java多线程(一)——Hotspot的锁( Synchronized)

news2025/1/21 6:38:29

1. 锁的概念

Java语言为了解决并发编程中存在的原子性、可见性和有序性问题,提供了一系列和并发处理相关的关键字,比如synchronized、volatile、final、concurren包等

2. Synchronized的基本使用

synchronized是Java提供的一个并发控制的关键字。主要用法有两种,分别是同步方法和同步代码块。

synchronized可以修饰方法,用法如下:

public class Demo{
    //普通方法
    public synchronized void functionA(){
        //...
    }
    
    //静态方法
    public static synchronized void functionB(){
        //...
    }
}

synchronized修饰普通方法时,对当前调用对象上锁,可以理解为:

synchronized(this)

synchronized修饰静态方法时,将对类本身上锁,可以理解为:

synchronized(Demo.class)

synchronized也可以修饰代码块,用法如下:

public class Demo{
    public void functionA(){
        Object obj = new Object();
        synchronized(obj){
            //...
        }
    }
}

synchronized修饰的代码块及方法,不允许被多个线程同时访问。

3. Synchronized实现原理

synchronized,是Java中用于并发情况下数据同步访问的一个重要关键字。当我们想要保证一个共享资源,在同一时间只会被一个线程访问到的时候,我们可以在底阿妈中使用synchronized关键字对类或者对象加锁。那么synchronized关键字到底如何实现上锁的呢?

3.1 Synchronized 修饰在代码块上

我们将下述代码进行反编译:

public class Main {
	
    public static void main(String[] args) {
        synchronized (Main.class) {
            System.out.println("hello synchronized");
        }
    }
}

反编译后,我们将得到如下的字节码指令:

public class com.company.Main {
  public com.company.Main();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  
  public static void main(java.lang.String[]);
    Code:
       0: ldc           #5                  // class com/company/Main
       2: dup
       3: astore_1
       4: monitorenter
       5: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       8: ldc           #6                  // String hello synchronized
      10: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      13: aload_1
      14: monitorexit
      15: goto          23
      18: astore_2
      19: aload_1
      20: monitorexit
      21: aload_2
      22: athrow
      23: return
          

我们主要关注 main() 方法中被synchronized修饰的代码块的字节码指令:

public class com.company.Main {
 
  public static void main(java.lang.String[]);
    Code:
       //...
       4: monitorenter
       //...
      14: monitorexit
       //...
      20: monitorexit

编译后的代码中,出现了一些特别的字节码指令,其中monitorenter字节码指令理解为加锁,monitorexit字节码指令理解为释放锁。每个对象维护者一个记录着被锁次数的计数器(它将被记录在MarkWord中,后续会谈到)。

代码中有两处monitorexit指令,第二处的monitorexit指令将在出现异常时被调用,进行锁的释放。

上述指令也反映了:synchronized修饰代码块的时候,不需要我们主动加锁和解锁的字段,因为它自动在代码块开始和结束的地方替我们补充了加锁和解锁的指令,这也是为什么synchronized还被称为内置锁。

3.2 Synchronized 修饰在方法上

synchronized除了修饰在代码块上,还可以修饰在方法上。如果synchronized修饰在方法上,同步方法的常量池中会有一个ACC_SYNCHRONIZED标志,当某个线程需要访问某个方法的时候,会检查是否有ACC_SYNCHRONIZED标志,如果有,则首先需要获得monitor锁(监视器锁),然后才开始进入临界区,执行方法,方法执行之后再释放monitor锁(监视器锁)。

同样的,如果方法执行过程中遇到异常,并且在方法内部并没有处理该异常,那么再异常被抛到方法外面之前,monitor锁(监视器锁)将会被自动释放。

4. Synchronized具有原子性与可见性,但没有有序性

4.1 原子性,可见性,有序性概念

原子性

原子性,即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行

Java内存模型中,有read,load等指令直接保证原子性操作,如果需要更大范围的原子性保证,则可以通过lockunlock来做块的同步,虚拟机提供了字节码指令monitorentermonitorexit来隐式地使用lockunlock这两个操作。反映到Java代码中,就是我们上述提到的synchronized关键字。

可见性

可见性,即当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。

Java内存模型通过:1)在变量修改后,将新值同步回主内存;2)在变量读取之前,从主存刷新变量的值。这两种方式来实现可见性。

Java可以通过volatile,synchronizedfinal来实现可见性。

有序性

有序性,即所有指令的执行顺序是有序的

Java可以通过volatile关键字来保证有序性。

4.2 Synchronized 具有原子性

synchronized修饰的临界区代码是具有原子性的。可能有多个线程争抢执行同一个临界区的代码,但当一个线程持有该临界区的锁时,即使其他正在争抢的线程被线程调度分到时间片,也无法进入临界区。所有其他线程都要等到当前持有锁的线程执行完临界区代码并退出,才有可能在被调度时,争抢到锁并执行临界区代码。

4.3 Synchronized 具有可见性

synchronized修饰的代码,会在开始执行的时候调用字节码指令monitorenter加锁,执行完成后,会调用字节码指令monitorexit解锁。

为了保证可见性,有一条规则是这样的:“在unlock之前,必须把变量同步回主内存中”,也就是在对一个变量解锁之前,必须先把该变量同步回主存中,这样解锁后,后续线程就可以访问到被修改后的值,从而保证了可见性。

4.4 Synchronized 没有有序性

由于处理器优化和指令重排等,CPU有可能对代码进行乱序执行。而需要注意的是,synchronized是无法禁止指令重排和处理器优化的。如果要阻止指令重排等来实现指令执行的有序性,则需要使用到volatile,这不是本章要讨论的内容,不做过多赘述。

5. 对象的内存布局与锁升级

5.1 普通对象和数组独享的内存布局图

对象在实例化之后,是被存放在堆内存中的。这里的对象由三部分组成,如下图:

请添加图片描述

左边是普通对象,对象头(Object header)包含着MarkWord和类型指针。而数组对象的对象头还多包含了数组长度。

成员变量存储的都是对象的真正的有效数据,也就是各个成员变量属性字段的值,如果拥有父类,还会包含父类的成员变量。

具体对齐填充的规则与JVM有关,在Hotspot中。对象的大小要求向8Byte对齐,当对象长度不足8字节的整数倍时,需要再对象中进行填充操作

5.1 Markword

在对象头中,MarkWord一共有64个bit,用于存储对象自身的运行时数据,标记对象处于一下状态中的某一种

请添加图片描述

在jdk6之前,通过synchronized关键字加锁的时候,无差别地使用重量级锁,重量级锁会使CPU在用户态和和心态之间频繁切换,有很大的系统消耗。后来随着synchronized的不断优化,提出了锁升级的概念,在MarkWord中,通过锁的标志位来表示当前对象的锁状态。

5.3 锁升级

我们知道了MarkWord可以用于锁升级,那么锁升级的过程是如何的呢?

synchronized不断优化中,引入了偏向锁,轻量级锁,重量级锁的概念,在不同的情况逐步进行锁膨胀。过程如下图:

请添加图片描述

5.3.1 偏向锁的开启

JVM参数可以开启偏向锁:

-XX:UseBiasedLocking

默认偏向锁在程序启动4s后开启。在JVM还没开启偏向锁的时候,如果用synchronized修饰,将会直接升级为轻量级锁。

当偏向锁开启后,对象实例化后,还没有线程对它上锁(还没有线程调用的方法对该实例使用synchronized修饰),将会是匿名偏向状态,线程ID(JavaThread* ) 指向 000…00000。也就是还没偏向任何一个线程。

当偏向锁开启后,第一个对该对象上锁(调用的方法中对该实例使用synchronized修饰)的线程,将会把线程ID(JavaThread* )记录到 MarkWord中。此时MarkWord的偏向锁位被置为 1,且锁标志位为 01。 此时这个对象就可以理解为偏向该线程。

需要注意的是,如果计算过对象的hashcode,则对象无法进入偏向状态,只能直接进入轻、重量级锁。在轻量级锁中,对象的hashcode存在Lock Record中,在重量级锁中,对象的hashcode被记录在Objectmonitor中。

问:为什么需要偏向锁?

答:有的对象可能大多使用的场合都只在一个线程中运行,只有少数情况会遇到多线程的竞争,我们不需要直接设置竞争机制,或者直接上消耗资源的锁。我们可以通过偏向锁这样,只在MarkWord上加一个线程ID的标记的低消耗的方式,来标志对象正在被使用。当遇到争抢的时候,再依情况进行锁升级。例如StringBuffer中很多方法都被synchronized修饰,但是它的使用场景经常只在一个线程中执行。

5.3.2 偏向锁->轻量级锁

当只有一个线程访问偏向锁的时候,仅仅是把线程ID记录到MarkWord中,没有额外的系统消耗。但是,当有新的线程也需要对该对象上锁,而且发现该对象已经被别的线程持有偏向锁(观察到MarkWord中有别的线程ID),此时为竞争升级为轻度竞争。

系统将会撤销偏向锁,并将锁升级为轻量级锁(自旋锁)。

轻量级锁简单地将对象头部作为指针,指向持有锁的线程堆栈的内部,来判断一个线程是否持有对象锁。线程的Lock Record记录着锁的记录,各个线程自旋争抢,把Lock Record写到MarkWord为争抢成功。别的线程只能CAS自旋,不断检查锁是否被释放。

问:有偏向锁,为什么还需要轻量级锁?

答:偏向锁在撤销的时候会消耗系统资源,在争抢激烈的时候,效率没有轻量级锁高。

问:为什么JVM默认一开始不打开偏向锁?

答:JVM启动过程中会有很多线程争抢,所以默认启动时不打开偏向锁,而是默认等待4s过后才打开偏向锁。

5.3.3 轻量级锁 -> 重量级锁

当发生重度竞争时,即1)有线程超过10次自旋, 2)自旋线程数超过了CPU核数的一般。JVM将会把锁升级为重量级锁。

重量级锁需要内核态的参与, JVM 向操作系统申请ObjectMonitor,这是一个C++对象,需要内核态才能访问。

重量级锁拥有等待队列,没有争抢到锁的线程不再自旋等待,而是直接进入阻塞状态,等待被唤醒。再次启动去争取锁的过程将比较耗时。

问:有轻量级锁,为什么还需要重量级锁?

答:线程自旋占CPU资源,重度竞争的时候,自旋的时间很长,或者自旋的线程很多,CPU花了更多时间在线程切换上。重量级锁通过内核态参与,将没有争抢到锁的线程阻塞,排队等待,减轻了系统资源的消耗。

5.3.4 代码中如何选择锁状态的?

我们前面说到,Java通过synchronized关键字对类或者对象上锁,在字节码指令中会有monitorentermonitorexit,在底层代码中,monitorenter首先判断偏向锁是否开启,尝试加偏向锁:

fast_enter();

如果成功,就将对象上偏向锁。

如果失败,将会尝试加轻量级锁:

slow_enter();

如果失败,将会直接通过内核态申请重量级锁:

inflate();

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/151886.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【LINUX】工具篇--gcc的使用

我们知道&#xff0c;在程序翻译的过程中一般会经过四个步骤预处理头文件展开&#xff0c;条件编译&#xff0c;宏替换&#xff0c;去注释编译C语言代码--->汇编代码汇编汇编代码--->可重定向目标二进制文件(只把自己写的函数形成二进制文件&#xff0c;此阶段无法被执行…

Vue3一学就会系列:02 模板语法与计算属性

系列文章目录 Vue3一学就会系列&#xff1a;01 vue3安装与搭建项目 文章目录系列文章目录文本插值html 插入属性绑定常用指令计算属性总结文本插值 最基本的数据绑定形式是文本插值&#xff0c;它使用的是“Mustache”语法 (即双大括号)&#xff1a; 知识点&#xff1a; {{}}…

(考研湖科大教书匠计算机网络)第一章概述-第二节:三种交换方式(电路交换、报文交换和分组交换)

文章目录一&#xff1a;电路交换&#xff08;Circuit Switching&#xff09;二&#xff1a;分组交换&#xff08;Packet Switching&#xff09;三&#xff1a;报文交换&#xff08;Message Switching&#xff09;四&#xff1a;三种交换方式对比&#xff08;1&#xff09;概述&…

一个自定义的html5视频播放器

// 功能:// 1.视频的播放与暂停(图标变化)// 2.总时间的显示// 3.当前时间的显示(进度)// 4.进度条的显示// 5.跳跃播放// 6.全屏<!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport"…

利用决策树学习基金持仓并识别公司风格类型

摘要与声明 1&#xff1a;本文主要利用决策树学习基金持仓并反向推理出一套更受市场认可的风格划分标准&#xff0c;最后借助该模型识别公司所属的风格类型&#xff1b; 2&#xff1a;本文主要为理念的讲解&#xff0c;模型也是笔者自建&#xff0c;文中假设与观点是基于笔者…

JVM的内存配置参数

VM的结构问题&#xff1a;JVM分两块&#xff1a;PermanentSapce和HeapSpace&#xff0c; HeapSpace 【old new{Eden&#xff0c;from&#xff0c;to}】 PermantSpace主要负责存放加载Class类级别的class本身&#xff0c;method&#xff0c;field等反射对象&#xff0c;一般不…

重磅指挥棒!2023年国资委央企指标考核体系从两利四率到一利五率变化解读

前几天&#xff0c;2023年的第三个工作日&#xff0c;国资委召开了中央企业负责人会议&#xff0c;提出了优化中央企业经营考核指标体系的六个指标 —— 一利五率&#xff0c;目标是一增一稳四提升。一增&#xff0c;就是确保利润总额增速高于全国GDP增速。一稳&#xff0c;资产…

小程序学习(1)-------小程序的结构及作用

获取APPID 开发-开发管理->开发设置&#xff08;新建项目时需要输入appid&#xff09; 小程序的文件结构 pages 用来存放所有小程序的页面 utils 用来存放工具性质的模块&#xff08;例如&#xff1a;格式化时间的自定义模块&#xff09; app.js 小程序项目的入口文件 ap…

【Redis】分别从互斥锁与逻辑过期两个方面来解决缓存击穿问题

文章目录前言一.什么是缓存击穿二.基于互斥锁解决缓存击穿三.基于逻辑过期解决缓存击穿四.接口测试五.两者对比前言 身逢乱世&#xff0c;未雨绸缪 一.什么是缓存击穿 说直白点&#xff0c;就是一个被非常频繁使用的key突然失效了请求没命中缓存&#xff0c;而因此造成了无数…

使用electron将vue项目打包成exe

文章目录一、前言二、实现方法1.跑通示例代码 electron-quick-start<1>clone示例代码<2>进入项目根目录&#xff0c;下载依赖<3>测试运行2.打包自己的 vue 项目3.将vue项目整合到示例代码中打包exe<1>将打包好的 dist 文件夹复制到示例代码 electron-q…

sklearn之OPTICS聚类

文章目录简介sklearn实现cluster_optics_dbscan简介 OPTICS算法&#xff0c;全称是Ordering points to identify the clustering structure&#xff0c;是一种基于密度的聚类算法&#xff0c;是DBSCAN算法的一种改进。 众所周知&#xff0c;DBSCAN算法将数据点分为三类&#…

ResNet精读(2)

FLOPs &#xff1a;整个网络要计算多少个浮点运算 卷积层的浮点运算等价于 输入的高*输入的宽*通道数*输出通道数再乘以卷积核的高和宽再加上全连接的一层 我们发现训练的时候的精度是要比测试精度来的高的在一开始&#xff0c;这是因为训练的时候用了数据增强 使得训练误差…

2022年莱佛士大盘点 ,设计的种子遍地开花!

2022似乎过得尤其之快&#xff0c;反复的居家隔离和线上网课&#xff0c;似乎给2022蒙上了一层雾蒙蒙的灰色。但2022总还给我们留下了些东西&#xff0c;在莱佛士设计学院&#xff0c;我们共同见证了梦想的种子在设计的各个领域遍地开花。现在我们一起来看看2022年莱佛士学生们…

广义表——LISP的基石

线性表中存放的是同一类型的元素&#xff0c;而广义表是线性表的推广&#xff0c;即广义表中除包含类型相同的元素外&#xff0c;还可以包含具有其自身结构的元素。在人工智能领域使用十分广泛的 LISP语言中&#xff0c;广义表是一种基本数据类型&#xff0c;LISP 语言中的数据…

Vue3案例-todoMVC-pinia版 (可跟做练手)

列表展示功能 &#xff08;1&#xff09; 在main.js中引入pinia import { createApp } from vue import App from ./App.vue import { createPinia } from pinia import ./styles/base.css import ./styles/index.cssconst pinia createPinia() createApp(App).use(pinia).m…

Spring源码学习~11、Bean 的加载步骤详解(二)

Bean 的加载步骤详解&#xff08;二&#xff09; 一、循环依赖 1、什么是循环依赖 循环依赖就是循环引用&#xff0c;即两个或多个 bean 互相之间持有对方&#xff0c;如下图&#xff1a; 循环引用不是循环调用&#xff0c;循环调用是方法之间的环调用&#xff0c;循环调用是…

谷粒学院——Day18【权限管理Spring Security、配置中心Nacos、代码托管git】

❤ 作者主页&#xff1a;欢迎来到我的技术博客&#x1f60e; ❀ 个人介绍&#xff1a;大家好&#xff0c;本人热衷于Java后端开发&#xff0c;欢迎来交流学习哦&#xff01;(&#xffe3;▽&#xffe3;)~* &#x1f34a; 如果文章对您有帮助&#xff0c;记得关注、点赞、收藏、…

Linux搭建Gitlab保姆级教程

文章目录1、gitlab安装1.1、gitlab介绍1.1.1、概念1.1.2、gitlab与github的区别1.1.3、gitlab的优势1.1.4、gitlab主要服务构成1.1.5、gitlab的工作流程1.2、准备工作1.3、安装1.4、配置1.5、启动1.6、测试2、gitlab安装目录3、gitlab常用命令4、注册账号5、gitlab相关设置5.1、…

上半年要写的博客文章23

上半年要写的博客文章21 这里写自定义目录标题欢迎使用Markdown编辑器新的改变功能快捷键合理的创建标题&#xff0c;有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片生成一个适合你的列表创建一个表格设定内容居中、居左、居右SmartyPants创建一个…

ArcGIS基础实验操作100例--实验76按格网统计点要素

本实验专栏参考自汤国安教授《地理信息系统基础实验操作100例》一书 实验平台&#xff1a;ArcGIS 10.6 实验数据&#xff1a;请访问实验1&#xff08;传送门&#xff09; 高级编辑篇--实验76 按格网统计点要素 目录 一、实验背景 二、实验数据 三、实验步骤 &#xff08;1&…