Java并发编程-synchronized、volatile、AQS解析

Java并发编程

synchronized 如何保证线程安全

JDK1.6 之前,synchronized 是一个重量级锁相比于JUC的锁显得非常笨重,存在性能问题

JDK1.6 及之后,Java 对 synchronized 进行的了一系列优化,性能与 JUC 的锁不相上下

synchronized 可以保证并发程序的 原子性、可见性、有序性

synchronized 可以修饰方法和代码块

synchronized如何保证可见性?

JMM 关于 synchronized 的两条规定:

  • 线程解锁前:必须把自己本地内存中共享变量的最新值刷新到主内存中
  • 线程加锁时:将清空本地内存中共享变量的值,从而使用共享变量时需要从主内存中重新读取最新的值

synchronized 实现可见性的过程为:

  1. 同步获取锁
  2. 清空线程本地内存变量
  3. 从主内存拷贝最新变量副本到线程本地内存
  4. 执行代码
  5. 将更改后的变量副本从线程本地内存刷新到主内存中
  6. 释放锁

synchronized如何保证同步呢?

同步操作主要由两个 jvm 指令实现:monitorenter、monitorexit

对于下边代码:

public class LockMain {
    public synchronized void insert() {
        System.out.println("synchronized 方法");
    }
    public void select() {
        synchronized (this) {
            System.out.println("synchronized 块");
        }
    }
}

在该类所在的路径,打开命令行执行:

# 先编译成字节码
javac .\LockMain.java
# 再通过 javap 指令反编译出来 JVM 指令 
# -v 可以输出更多详细信息
javap -v .\LockMain.class

反编译后,两个方法的 JVM 指令如下:

public synchronized void insert();
  descriptor: ()V
  flags: ACC_PUBLIC, ACC_SYNCHRONIZED
  Code:
     0: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
     3: ldc           #13                 // String synchronized 方法
     5: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
     8: return

public void select();
  descriptor: ()V
  flags: ACC_PUBLIC
  Code:
     0: aload_0
     1: dup
     2: astore_1
     3: monitorenter 					  // monitorenter 指令进入同步代码块
     4: getstatic     #7                  // Field java/lang/System.out:Ljava/io/PrintStream;
     7: ldc           #21                 // String synchronized 块
     9: invokevirtual #15                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
    12: aload_1
    13: monitorexit						  // monitorexit 指令退出同步代码块
    14: goto          22
    17: astore_2
    18: aload_1
    19: monitorexit                       // monitorexit 指令退出同步代码块
    20: aload_2
    21: athrow
    22: return

  • synchronized 加在方法上,可以看到 insert 方法的 flags 有一个 ACC_SYNCHRONIZED 关键字,那么 JVM 进行方法调用时,发现该关键字,就会先获取锁,再执行方法,底层也是基于 monitorenter 和 monitorexit 实现的

  • synchronized 加在代码块上,有一个 monitorenter 对应了两个 monitorexit,这是因为编译器会为同步代码块添加一个隐式的 try-finally,在 finally 中也会调用 monitorexit 释放锁

管程

Java 中提供的并发包都是以管程技术为基础的,管程就是一把解决并发问题的万能钥匙

Java 采用的管程技术在哪里体现了呢?synchronized 关键字以及 wait()、notify()、notifyAll() 都是管程的组成部分

管程解决互斥问题的思路:将共享变量以及对共享变量的操作统一封装起来

synchronized 锁的优化

JDK1.6 之前,synchronized 使用重量级锁,性能开销很高

JDK1.6 引入了锁的优化:偏向锁和轻量级锁

同步锁共有 4 个状态:无锁、偏向锁、轻量级锁、重量级锁,这 4 个状态会随着竞争激烈而逐渐升级

偏向锁

当一个线程第一次竞争到锁,则拿到的就是偏向锁,此时不存在其他线程的竞争

偏向锁的性能是很高的,他会偏向第一个访问锁的线程,持有偏向锁的线程不需要触发同步,连 CAS 操作都不需要

JDK15 以后逐渐废弃偏向锁,因为维护的开销比较大

轻量级锁

在线程竞争不太激烈的情况下,并且线程持有锁时间极短,本质就是 CAS 自旋

重量级锁

当 CAS 自旋达到一定次数,就会升级为重量级锁

这几种锁的状态存储在了对象头的 Mark Word 中,并且还指向了持有当前对象锁的线程

synchronized 加锁流程如下:

在这里插入图片描述

volatile

synchronized 在多线程场景下存在性能问题

volatile 关键字是一个更轻量级的线程安全解决方案

volatile 关键字的作用:保证多线程场景下变量的可见性和有序性

  • 可见性:保证此变量的修改对所有线程的可见性。
  • 有序性:禁止指令重排序优化,编译器和处理器在进行指令优化时,不能把在 volatile 变量操作(读/写)后面的语句放到其前面执行,也不能将volatile变量操作前面的语句放在其后执行。遵循了JMM 的 happens-before 规则

线程写 volatile 变量的过程:

  1. 改变线程本地内存中volatile变量副本的值;
  2. 将改变后的副本的值从本地内存刷新到主内存

线程读 volatile 变量的过程:

  1. 从主内存中读取volatile变量的最新值到线程的本地内存中
  2. 从本地内存中读取volatile变量的副本

volatile 实现原理

volatile 实现可见性就是基于 内存屏障 实现的

内存屏障是一种 CPU 指令,用于控制特定条件下的重排序和内存可见性问题

  • 写操作时,在写指令后边加上 store 屏障指令,让线程本地内存的变量能立即刷到主内存中
  • 读操作时,在读指令前边加上 load 屏障指令,可以及时读取到主内存中的值

JMM 中有 4 类内存屏障:(Load 操作是从主内存加载数据,Store 操作是将数据刷新到主内存)

  • LoadLoad:确保该内存屏障前的 Load 操作先于屏障后的所有 Load 操作。对于屏障前后的 Store 操作并无影响屏障类型

  • StoreStore:确保该内存屏障前的 Store 操作先于屏障后的所有 Store 操作。对于屏障前后的Load操作并无影响

  • LoadStore:确保屏障指令之前的所有Load操作,先于屏障之后所有 Store 操作

  • StoreLoad:确保屏障之前的所有内存访问操作(包括Store和Load)完成之后,才执行屏障之后的内存访问操作。全能型屏障,会屏蔽屏障前后所有指令的重排

volatile 的缺陷就是不能保证变量的原子性

解决方案:可以通过加锁或者 AtomicInteger原子操作类来保证该变量操作时的原子性

public static AtomicInteger count = new AtomicInteger(0);

CAS

同步组件中大量使用 CAS 技术实现了 Java 多线程的并发操作。整个 AQS 同步组件、Atomic 原子类操作等等都是以 CAS 实现的

Java 中 ConcurrentHashMap 在 jdk1.8 的版本中也调整为了 CAS+Synchronized。可以说 CAS 是整个 JUC 的基石

CAS 操作主要涉及 3 个操作数:

  1. V:要写的内存地址
  2. E:预期值
  3. N:新写入的值

当内存地址的值等于预期值时,将该内存地址的值写为新的值

Java 中的 CAS 通过 Unsafe 类实现

CAS 缺陷:

  1. 循环时间过长:如果 CAS 自旋一直不成功,会给 CPU 带来很大开销

  2. 只能针对一个共享变量

  3. 存在 ABA 问题:CAS 只检查了值有没有发生改变,如果原本值为 A,被改为 B 之后,又被改为了 A,那么 CAS 是不会发现值被改编过了的

    ABA 问题解决方案:为每个变量绑定版本号,A–>B–>A 加上版本号为:A1–>B2–>A3

Lock 锁与 AQS

JUC 包中提供的锁有:

  • ReentrantLock 重入锁
  • ReentrantReadWriteLock 读写锁
  • StampedLock 重入读写锁,JDK1.8 引入

AQS 是抽象队列同步器,是 JUC 中的核心基础组件,AQS 是一个 FIFO 的双向队列,队列中存储的是 thread,JUC 中大部分同步工具类都是基于 AQS 的

线程在获取锁失败之后,会被封装成 Node 节点假如到 AQS 阻塞等待,当获取锁的线程释放锁之后,会从 AQS 队列中唤醒一个线程,AQS 队列如下:

在这里插入图片描述

ReentrantLock 加锁流程分析如下:

AQS源码详细解析参考文章

在这里插入图片描述

synchronized 的缺陷

synchronized 锁缺陷:

  1. 无法控制线程阻塞时长
  2. 线程阻塞等待锁的状态不可以中断
  3. 读多写少的情况下,synchronized 的性能很差

上边这几种缺陷都在 JUC 得到解决,JUC 提供了 tryLock() 方法来指定了线程阻塞获取锁的时间,也有读写锁提高读多写少情况下的性能,并提供了 lockInterruptibly() 方法,让线程在阻塞获取锁时可以被中断

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

python源码,在线读取传奇列表,并解析为需要的JSON格式

python源码,在线读取传奇列表,并解析为需要的JSON格式 [Server] ; 使用“/”字符分开颜色,也可以不使用颜色,支持以前的旧格式,只有标题和服务器标题支持颜色 ; 标题/颜色代码(0-255)|服务器标题/颜色代码(0-255)|服务…

WPF使用WebBrowser页面白屏,不显示渲染页面问题排查

前言 WPF使用WebBrowser页面白屏&#xff0c;不显示渲染页面问题排查 代码 <Window x:Class"WpfApp1.Window5"xmlns"http://schemas.microsoft.com/winfx/2006/xaml/presentation"xmlns:x"http://schemas.microsoft.com/winfx/2006/xaml"x…

Kubernetes入门笔记 ——(3)理解pod对象

为什么需要pod 最为熟知的一句话&#xff1a;pod是k8s的最小调度单位。刚开始听到这句话时会想&#xff0c;已经有容器了&#xff0c;k8s为什么还要搞个pod出来&#xff1f;容器和pod是什么关系&#xff1f;容器的本质是进程&#xff0c;而k8s本质上类似操作系统。 熟悉Linux的…

在git使用SSH密钥进行github身份认证学习笔记

1.生成ssh密钥对 官网文档&#xff1a;Https://docs.github.com/zh/authentication&#xff08;本节内容对应的官方文档&#xff0c;不清晰的地方可参考此内容&#xff09; 首先&#xff0c;启动我们的git bush&#xff08;在桌面右键&#xff0c;点击 Git Bush Here &#xf…

SystemVerilog学习(0)——目录与传送门

一、验证导论 SystemVerilog学习&#xff08;1&#xff09;——验证导论-CSDN博客文章浏览阅读403次。SystemVerilog自学&#xff0c;验证系统概述&#xff0c;什么是SVhttps://blog.csdn.net/apple_53311083/article/details/133953016 二、数据类型 SystemVerilog学习&…

非标设计之气缸的选型三

一、气缸正确安装方式&#xff0c;气缸调试注意事项 气缸安装方式&#xff1a; 气缸正确安装方式&#xff1a; NB(无支架) FB(脚座支架) FR(法兰) FA(前法兰) FB(后法兰) SC(单耳支架) DC(双耳支架) CT(耳轴支架) 气缸的安装形式决定方法&#xff1a;根据负荷运动方向决定气缸…

二百一十二、Flume——Flume实时采集Linux中的目录文件写入到HDFS中(亲测、附截图)

一、目的 在实现Flume实时采集Linux中的Hive日志写入到HDFS后&#xff0c;再做一个测试&#xff0c;用Flume实时采集Linux中的目录文件&#xff0c;即使用 Flume 监听Linux整个目录的文件&#xff0c;并上传至 HDFS中 二、前期准备 &#xff08;一&#xff09;安装好Hadoop、…

微信个人号机器人开发

简要描述&#xff1a; 取消消息接收 请求URL&#xff1a; http://域名地址/cancelHttpCallbackUrl 请求方式&#xff1a; POST 请求头Headers&#xff1a; Authorization&#xff1a;login接口返回Content-Type&#xff1a;application/json 无参数 返回数据&#xff…

PADS9.5封装库转换为AD库

1、打开PADS Layout&#xff0c;File – Library&#xff0c;选中usr&#xff0c;如下图&#xff1a; 2、封装– 导入&#xff0c;选中你的 .d后缀文件(也就是PADS的封装文件)&#xff0c;打开。 3、元件 – 新建 – PCB封装 - 分配 - 确定。 4、&#xff0c;选择“斜线”…

vue3路由跳转页面到顶部,Stable Diffusion使用ControlNet

打开新页面时&#xff0c;页面自动回到顶部 在router.js页面添加 router.beforeEach((to, from, next) > {// 在每次导航之前滚动到页面顶部window.scrollTo({top: 0,behavior: smooth // 可选的&#xff0c;使滚动平滑进行});next(); }); Controlnet ControlNet 是用来…

RocketMQ-源码架构

源码环境搭建 1、主要功能模块 RocketMQ官方Git仓库地址&#xff1a;GitHub - apache/rocketmq: Apache RocketMQ is a cloud native messaging and streaming platform, making it simple to build event-driven applications. RocketMQ的官方网站下载&#xff1a;下载 | R…

001 LLM大模型之Transformer 模型

参考《大规模语言模型--从理论到实践》 目录 一、综述 二、Transformer 模型 三、 嵌入表示层&#xff08;位置编码代码&#xff09; 一、综述 语言模型目标是建模自然语言的概率分布&#xff0c;在自然语言处理研究中具有重要的作用&#xff0c;是自然 语言处理基础任务之一…

【软考中级——软件设计师】备战经验 笔记总结分享

考试成绩 我第一次备考是在2022 然后那时候取消了这次是第二次 靠前我一个月复习的看了以前的笔记 然后刷了七八道历年题目学习资料推荐 &#xff1a;zst——2021 b站链接自荐一下我的笔记 &#xff1a; 软考笔记专栏 视频确实很长 &#xff0c; 我的建议就是先看笔记 然后不会…

深度学习之全面了解网络架构

在这篇文章中&#xff0c;我们将和大家探讨“深度学习中的网络架构”这个主题&#xff0c;解释相关背景知识&#xff0c;并就一些问题进行解答。 我选择的问题反映的是常见用法&#xff0c;而不是学术用例。我将概括介绍该主题&#xff0c;然后探讨以下四个问题&#xff1a; …

泽攸科技二维材料转移台的应用场景及优势

随着二维材料的广泛研究和各种潜在应用的开发&#xff0c;对于二维材料样品的精密操控与转移的需求日益增加。特别是一些新型二维材料的制备和器件集成制备中&#xff0c;需要在显微镜下对样品进行观察与定位&#xff0c;并能够在微米甚至纳米量级上精确移動和转移样品。 传统…

做数据分析为何要学统计学(6)——什么问题适合使用方差分析?

方差分析&#xff08;ANOVA&#xff0c;也称变异数分析&#xff09;是英国统计学家Fisher&#xff08;1890.2.17&#xff0d;1962.7.29&#xff09;提出的对两个或以上样本总体均值进行差异显著性检验的方法。 它的基本思想是将测量数据的总变异&#xff08;即总方差&#xff…

Go语言初始化数组的六种方式

介绍 在Go语言中&#xff0c;有多种方式可以初始化数组&#xff0c;本文将介绍初始化数组的六种方法。 方式1&#xff1a;指定数组大小并初始化 var array [3]int [3]int{1, 2, 3}指定数组的大小为3&#xff0c;并初始化为指定的值1, 2, 3。 方式2&#xff1a;根据初始化值…

Linux 中用户与权限

目录 1.添加用户 useradd 2.修改用户属性 usermod 3.用户组管理 3.1新建组 3.2 组和用户的关系 3.3 修改组名 3.4 删除组 4.权限管理 4.1.文件类型 4.2 权限 4.3 修改文件权限 1.添加用户 useradd 1&#xff09;创建用户 useradd 用户名 2&#xff09;设置用户密码…

Vue3问题:如何在页面上添加水印?

前端功能问题系列文章&#xff0c;点击上方合集↑ 序言 大家好&#xff0c;我是大澈&#xff01; 本文约3100字&#xff0c;整篇阅读大约需要5分钟。 本文主要内容分三部分&#xff0c;如果您只需要解决问题&#xff0c;请阅读第一、二部分即可。如果您有更多时间&#xff…

【npm | npm常用命令及镜像设置】

npm常用命令及镜像设置 概述常用命令对比本地安装全局安装--save &#xff08;或 -S&#xff09;--save-dev &#xff08;或 -D&#xff09; 镜像设置设置镜像方法切换回npm官方镜像选择镜像源 主页传送门&#xff1a;&#x1f4c0; 传送 概述 npm致力于让 JavaScript 开发变得…