Java内存模型和线程安全

news2024/9/21 10:54:29

Java内存模型和线程安全

  • Java内存模型
    • 引言
    • volatile关键字
    • synchronized关键字
    • Java线程
  • Java线程安全
  • synchronized锁优化
    • 锁优化技巧列举
      • 自旋锁
      • 锁消除
      • 锁粗化
    • 具体实现
      • 轻量级锁
      • 偏向锁

Java内存模型

引言

在这里插入图片描述

对于多核处理器而言,每个核都会有自己单独的高速缓存,又因为这多个处理器共享同一块主内存,为了在并行运行的情况下,包装各个缓存中缓存的结果的一致性,需要引用缓存一致性协议。

注意: 处理器只和自己的高速缓存交换,如果修改了高速缓存中的数据,就需要同步回主内存,并且通过缓存一致性协议让其他核的高速缓存失效。

高速缓存的出现主要是为了解决CPU运算速度和主内存速度不匹配而引入的缓冲模块


在这里插入图片描述

上图是java的内存模型,Java线程的数据读写都只能从工作内存获取,不同线程的工作内存是隔离的、

此处的工作内存主要对应线程私有的虚拟机栈部分,而主内存则对应Java堆中的对象实例数据部分。

同样JVM也必须通过一种一致性协议来保证多个工作内存间的数据一致性问题。


volatile关键字

volatile具有两个作用:

  • volatile关键字修饰的变量对所有线程立马可见
  • 禁止指令重排优化

为什么volatile变量具有上面两个作用呢? 是因为Java内存模型中对volatile变量定义了特殊处理规则:

  • 每次使用volatile变量前都必须从主内存中获取最新结果
  • 每次修改volatile变量后都必须立刻同步到主内存中
  • volatile修饰的变量不会被指令重排序优化

目前还存在两个问题,我们依次来解决一下:

  • 防止指令重排序如何实现的 ?

在这里插入图片描述

对应汇编代码:

在这里插入图片描述

volatile修饰的变量,赋值后,会生成一个lock addl指令,该指令作用是将本处理器的缓存写入内存,该写入通过缓存一致性协议也会使得其他高速缓存失效。

因为lock alld指令把修改同步到内存,那么意味着所有之前的操作都已经执行完成了,这样便形成了指令重排序无法越过内存屏障的效果。

指令重排序只会在多线程情况下存在并发问题

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zgJi8zx8-1673796664803)(Java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B.assets/image-20230115225020488.png)]


  • volatile修饰的变量一定是并发安全的吗?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IqHUWwPD-1673796664803)(C:/Users/zdh/AppData/Roaming/Typora/typora-user-images/image-20230115223112467.png)]

volatile修饰符提供的两个作用并没有体现出其一定是并发安全的,上面的例子也证明了,那么为什么呢?

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AoGABo1U-1673796664803)(C:/Users/zdh/AppData/Roaming/Typora/typora-user-images/image-20230115223232246.png)]

一个自增操作在字节码层面就对应字条指令,如果在算上解释执行每条指令需要使用到多行代码,或者编译执行对应多条机器码,那么光一个自增操作底层可能就需要执行十几条c代码或者几十条汇编指令,并且不保证这些c代码和汇编指令的原子执行,因此不是线程安全的。


synchronized关键字

synchronized关键字具有可见性,因为对一个变量执行lock前,必须从主内存获取最新值,对一个变量执行unlock前,必须先把此变量同步回主内存。


Java线程

线程调度方式:

  • 协同式调度: 线程执行时间由线程本身控制
  • 抢占式调度: 线程执行时间由系统进行分配

java线程采用1:1内核线程方式实现,因此采用的是抢占式调度。

状态转换:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sp6IrVr0-1673796664804)(Java%E5%86%85%E5%AD%98%E6%A8%A1%E5%9E%8B.assets/image-20230115232325626.png)]


Java线程安全

  • 不可变对象一定是线程安全的,如: String,Integer等
  • synchronized关键字实现互斥同步: 通过monitorEnter和monitorExit两个字节码指令完成抢锁和释放锁步骤,释放锁由JVM保证一定能够完成

在这里插入图片描述

  • java的Lock接口,在java层面实现的互斥锁,如: ReentrantLock, 优势: 等待可中断,实现了公平抢锁机制,可以关联多个条件队列。劣势: 需要在finally块中手动释放锁。
  • 如果要实现非阻塞同步,可以使用CAS加重试机制,CAS需要硬件方面提供支持,如: java提供的原子类,AtomicInteger 。CAS会产生ABA问题,可以通过添加版本号解决,如: AtomicStampedReference。
  • 对于不需要任何同步的场景有: 可重入代码(结果可预测),线程本地存储: ThreadLocal

在这里插入图片描述


synchronized锁优化

‘synchronized锁在jdk5之前最大的问题在于一旦抢不到锁就需要阻塞,而不能通过短暂的重试机制来避免阻塞造成的线程切换, 因为java线程是直接映射到内核线程的,因此线程上下文切换开销很大。

锁优化技巧列举

自旋锁

核心思想: 如果抢锁失败,那么通过CAS重新尝试多次,如果成功,那么就避免了阻塞造成的线程上下文切换,如果失败,那么就进行阻塞等待。

JDK 1.4.2中引入了最简单的自旋锁实现,说它简单是因为它的自旋次数默认是十次,不会根据运行状态动态变更。

JDK 6中引入了自适应自旋锁,相比于JDK 1.4.2简单的自旋锁实现而言,对自旋次数进行动态变更。

  • 如果上一次自旋等待过程中成功获得某个对象锁,那么这一次会动态调大自旋次数。
  • 如果多次自旋等待过程中都没能成功获得某个对象锁,那么下一次可能会跳转自旋过程,避免浪费吹起资源。

锁消除

虚拟机即时编译器运行时,对一些代码要求同步,但是通过逃逸分析检测发现当前方法内部所有在堆上的数据都不会逃逸出去,从而被其他线程访问到,那么就可以把它们认为是线程私有的,因此会消除该方法内部所有同步措施。

    public static String concatString(String ... str) {
        StringBuffer stringBuffer = new StringBuffer();
        for (String s : str) {
            stringBuffer.append(s);
        }
        return stringBuffer.toString();
    }

stringBuffer内部每个方法调用都会加锁,但是该方法本身执行是不存在线程安全问题的,因此可以忽略内部所有同步措施。


锁粗化

虽然推荐尽可能将同步范围缩小,但是如果某个方法内部存在下面的情况:

    private static Integer i = 0;

    private static void display1() {
        for (int i = 0; i < 10; i++) {
            synchronized (Main.class) {
                i++;
            }
        }
    }

    private static void display2() {
        synchronized (Main.class) {
            i++;
        }
        synchronized (Main.class) {
            i++;
        }
        synchronized (Main.class) {
            i++;
        }
    }

上面举例的方法中都存在反复对一个对象加锁和解锁的步骤,这样即使在没有线程竞争的情况下,频繁地进行互斥同步操作也会导致性能损耗,因此会进行锁粗化优化:

    private static Integer i = 0;

    private static void display1() {
        synchronized (Main.class) {
            for (int i = 0; i < 10; i++) {
                i++;
            }
        }
    }

    private static void display2() {
        synchronized (Main.class) {
            i++;
            i++;
            i++;
        }
    }

把多个同步代码块用一个同步块替换,如果是循环体内部的同步块,那么将同步块外提。


具体实现

synchronized锁最原始的实现如下:
在这里插入图片描述
该实现最大的问题在于忽略了多数同步代码块运行周期内是不存在竞争的,因此频繁的加锁和解锁设置也会导致性能损耗,并且还需要创建一个Monitor实例对象。

轻量级锁

因此,我们也称上面这种原始实现为重量级锁,为了对重量级锁进行优化,jvm推出了轻量级锁:

  • 轻量级锁的核心思想是在线程获取锁时只是简单标记一下锁被当前线程获取,而在释放锁时,再将标记移除
  • 如果当前线程持有轻量级锁期间出现了锁竞争情况,那么轻量级锁会退化为重量级锁

jvm轻量级锁和偏向锁实现都使用到了对象头,我们来看一下:
在这里插入图片描述
轻量级锁实现如下:

在代码即将进入同步块的时候,如果此同步对象没有被锁定(锁标志位为“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝(官方为这份拷贝加了一个Displaced前缀,即Displaced Mark Word),这时候线程堆栈与对象头的状态如图所示:
在这里插入图片描述
然后,虚拟机将使用CAS操作尝试把对象的Mark Word更新为指向Lock Record的指针。如果这个更新动作成功了,即代表该线程拥有了这个对象的锁,并且对象Mark Word的锁标志位(Mark Word的最后两个比特)将转变为“00”,表示此对象处于轻量级锁定状态。这时候线程堆栈与对象头的状态如图所示:
在这里插入图片描述
如果这个更新操作失败了,那就意味着至少存在一条线程与当前线程竞争获取该对象的锁。虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧,如果是,说明当前线程已经拥有了这个对象的锁,那直接进入同步块继续执行就可以了,否则就说明这个锁对象已经被其他线程抢占了。如果出现两条以上的线程争用同一个锁的情况,那轻量级锁就不再有效,必须要膨胀为重量级锁,锁标志的状态值变为“10”,此时Mark Word中存储的就是指向重量级锁(互斥量)的指针,后面等待锁的线程也必须进入阻塞状态。


偏向锁

轻量级锁可以在没有线程竞争的情况下,避免创建对应的监视器对象,但是如果锁总是被一个线程获取,那么就没有必要在获取锁前打上标记,而释放锁前撤销标记了,可以只打一次标记,如果下次还是这个同一个线程来获取锁,那么就没必要重复进行打标记和释放标记了。

上述锁的实现思路被称为偏向锁,偏的意思就是锁一直没有被其他线程获取,那么持有偏向锁的线程永远不需要再进行同步。

偏向锁原理如下:

当锁对象第一次被线程获取的时候,虚拟机将会把对象头中的标志位设置为“01”、把偏向模式设置为“1”,表示进入偏向模式。同时使用CAS操作把获取到这个锁的线程的ID记录在对象的Mark Word之中。如果CAS操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作(例如加锁、解锁及对Mark Word的更新操作等)。

一旦出现另外一个线程去尝试获取这个锁的情况,偏向模式就马上宣告结束。根据锁对象目前是否处于被锁定的状态决定是否撤销偏向(偏向模式设置为“0”),撤销后标志位恢复到未锁定(标志位为“01”)或轻量级锁定(标志位为“00”)的状态,后续的同步操作就按照上面介绍的轻量级锁那样去执行。偏向锁、轻量级锁的状态转化及对象Mark Word的关系如图所示:

在这里插入图片描述
在Java语言里面一个对象如果计算过哈希码,就应该一直保持该值不变(强烈推荐但不强制,因为用户可以重载hashCode()方法按自己的意愿返回哈希码),否则很多依赖对象哈希码的API都可能存在出错风险。而作为绝大多数对象哈希码来源的Object::hashCode()方法,返回的是对象的一致性哈希码(Identity Hash Code),这个值是能强制保证不变的,它通过在对象头中存储计算结果来保证第一次计算之后,再次调用该方法取到的哈希码值永远不会再发生改变。

因此,当一个对象已经计算过一致性哈希码后,它就再也无法进入偏向锁状态了;而当一个对象当前正处于偏向锁状态,又收到需要计算其一致性哈希码请求时,它的偏向状态会被立即撤销,并且锁会膨胀为重量级锁。

在重量级锁的实现中,对象头指向了重量级锁的位置,代表重量级锁的ObjectMonitor类里有字段可以记录非加锁状态(标志位为“01”)下的Mark WVord,其中自然可以存储原来的哈希码。

偏向锁可以提高带有同步但无竞争的程序性能,但它同样是一个带有效益权衡(Trade Off)性质的优化,也就是说它并非总是对程序运行有利。如果程序中大多数的锁都总是被多个不同的线程访问,那偏向模式就是多余的。在具体问题具体分析的前提下,有时候使用参数-XX:—UseBiasedLocking来禁止偏向锁优化反而可以提升性能。

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

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

相关文章

JavaWeb-会话技术

JavaWeb-会话技术 1&#xff0c;会话跟踪技术的概述 对于会话跟踪这四个词&#xff0c;我们需要拆开来进行解释&#xff0c;首先要理解什么是会话&#xff0c;然后再去理解什么是会话跟踪: 会话:用户打开浏览器&#xff0c;访问web服务器的资源&#xff0c;会话建立&#xff…

反射机制.

文章目录概述两个疑问关于java.lang.Class的理解获取Class实例的方式哪些类型可以有Class对象了解类的加载器掌握加载配置文件的另一种方式创建运行时类的对象体会动态性获取运行时类的完整结构调用运行时类的制定结构每日一考动态代理概述 1、反射是动态语言的关键 2、动态语…

使用Docker打包镜像并发布

1、docker介绍 Docker 是一个开源的应用容器引擎&#xff0c;以镜像的形式进行发布。docker的图标是一个大鲸鱼驮着许多集装箱在海上航行。大鲸鱼就是docker&#xff0c;集装箱就是一个个容器。容器是完全使用沙箱机制&#xff0c;相互之间不会有任何接口&#xff0c;每个容器都…

高级Spring之BeanFactory 与 ApplicationContext 的区别

ApplicationContext接口 SpringBootApplication public class A01 {private static final Logger log LoggerFactory.getLogger(A01.class);public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, IOException {//启动SpringBoot程序…

Python Scipy 显著性检验

Scipy 显著性检验显著性检验&#xff08;significance test&#xff09;就是事先对总体&#xff08;随机变量&#xff09;的参数或总体分布形式做出一个假设&#xff0c;然后利用样本信息来判断这个假设&#xff08;备择假设&#xff09;是否合理&#xff0c;即判断总体的真实情…

Linux基本功系列之userdel命令实战

春节马上要到了&#xff0c;大街上到处都是张灯结彩&#xff0c;喜气洋洋的景象&#xff0c;你们那里也开始了吗&#xff1f; 文章目录一. userdel命令概述二. 语法格式及常用选项语法格式及常用参数三. 参考案例3.1 删除用户但不删除家目录等相关的文件3.2 把用户彻底删除3.3 …

【Linux05-进程控制】进程创建、进程等待、进程退出、进程程序替换(附简易shell实现)

前言 本期分享进程控制的内容。 博主水平有限&#xff0c;不足之处望请斧正&#xff01; 进程的控制主要分为四点&#xff1a; 进程创建进程退出进程等待进程程序替换 进程创建 怎么创建 通过fork创建。 #fork 是什么 创建子进程的函数。&#xff08;使用已经介绍过&am…

Python基础学习 -- 概念

一、变量python的变量定义起来比较随意&#xff0c;不用定义数据类型a123b"123"系统会自动识别a为数值&#xff0c;b为字符串二、关键字定义变量名字的时候&#xff0c;要避开下面的关键字&#xff0c;但是可以通过大小写区分&#xff0c;as123;#错误定义As123;print…

阿里云服务器ECS

云服务器 ECS云服务器ECS&#xff08;Elastic Compute Service&#xff09;是一种简单高效、处理能力可弹性伸缩的计算服务。帮助您构建更稳定、安全的应用&#xff0c;提升运维效率&#xff0c;降低IT成本&#xff0c;使您更专注于核心业务创新。为什么选择云服务器ECS选择云服…

音频如何分割成两段音频?这些实用方法值得收藏

有些时候&#xff0c;我们从网上下载的音频素材可能会出现体积较大、播放时间长等情况&#xff0c;而我们却只需要其中的一小段。这个时候我们就需要借助一些音频分割软件来将重要的音频片段提取出来&#xff0c;从而有助于缩小音频文件的占比以及存储。那么如何如何分割音频呢…

JVM进修之路(一)程序计数器与虚拟机栈

JVM 定义&#xff1a;JVM:Java Virtual Machine&#xff0c;也就是Java运行时所需要的环境&#xff08;Java二进制字节码运行时所需要的环境&#xff09; 好处&#xff1a; 1.java代码一次编写&#xff0c;跨平台运行 2.自动内存管理&#xff0c;垃圾回收 3.数组下标越界检查 4…

千锋Node.js学习笔记

千锋Node.js学习笔记 文章目录千锋Node.js学习笔记写在前面1. 认识Node.js2. NVM3. NPM4. NRM5. NPX6. 模块/包与CommonJS7. 常用内置模块1. url2. querystring3. http4. 跨域jsonpcorsmiddleware&#xff08;http-proxy-middleware&#xff09;5. 爬虫6. events7. File System…

Mysql常用命令练习(一)

Mysql常用命令练习&#xff08;一&#xff09;一、数据库的三层结构二、数据库2.1、创建数据库2.2、查看、删除数据库2.3、备份和恢复数据库三、表3.1、创建表mysql常用的数据类型(列类型)创建表查看表查看表结构练习3.2、修改表修改表名修改表的字符集添加列修改列删除列练习3…

轻量级网络模型ShuffleNet

在学习ShuffleNet内容前需要简单了解卷积神经网络和MobileNet的相关内容&#xff0c;大家可以去看我之前的一篇博客MobileNet发展脉络&#xff08;V1-V2-V3&#xff09;&#xff0c;&#x1f197;&#xff0c;接下来步入正题~卷积神经网络被广泛应用在图像分类、目标检测等视觉…

易盾sdk引起项目的整体耗时问题?

大家好&#xff1a; 我是烤鸭。今年年初的时候&#xff0c;项目接入易盾sdk之后&#xff0c;随着接口调用次数增多(用到易盾sdk的接口)&#xff0c;项目整体性能变差。写篇文章做个复盘记录&#xff0c;其实同事已经写过了&#xff0c;我借鉴部分再拓展一些。 问题描述 突然收…

【JavaEE初阶】第五节.多线程 ( 基础篇 ) 线程安全问题(上篇)

目录 文章目录 前言 一、线程安全的概述 1.1 什么是线程安全问题 1.2 存在线程安全问题的实例 二、线程安全问题及其解决办法 2.1 案例分析 2.2 造成线程不安全的原因 2.3 线程加锁操作解决原子性 问题 &#xff1b; 2.3.1 什么是加锁 2.3.2 使用 synchronized关键字…

爆品跟卖商家必读:2023年快速入局TikTok选品5大关键

TikTok商业进程一直有在发展&#xff0c;开启东南亚小店&#xff0c;美国小店内邀……有吸引了不少外贸工厂和传统跨境电商卖家等玩家入局。2022年这一年&#xff0c;不管是直播带货&#xff0c;短视频带货&#xff0c;还是广告投流&#xff0c;数据都有新的变化。据报道&#…

Word 允许西文在单词中间换行,没用/无效 终极办法

有时在写论文中&#xff0c;英文的调整相当麻烦&#xff0c;为了节约版面&#xff0c;会设置允许西文在单词中间换行。但有时不希望这样&#xff0c;特别是在复制网上英文时&#xff0c;会出现单词分断换行情况&#xff0c;如何解决&#xff1a; 1.一般办法。 在Word选择要调整…

C规范编辑笔记(十)

往期文章&#xff1a; C规范编辑笔记(一) C规范编辑笔记(二) C规范编辑笔记(三) C规范编辑笔记(四) C规范编辑笔记(五) C规范编辑笔记(六) C规范编辑笔记(七) C规范编辑笔记(八) C规范编辑笔记(九) 正文&#xff1a; 又是新的一年&#xff0c;2023年的第一篇没想到隔了这么久…

MyBatis-Plus加密字段查询(密文检索)

MyBatis-Plus数据安全保护(加密解密)解释说明 1.字段加密后&#xff0c;数据库存储的字段内容为十六进制格式的密文2.条件查询时&#xff0c;若不对密文进行处理将无法匹配出想要的结果3.处理方式是借助SQL的AES_DECRYPT函数将密文解密后匹配4.SQL的解密函数只有AES_DECRYPT&am…