【多线程及高并发 三】volatile synchorized 详解

👏作者简介:大家好,我是若明天不见,BAT的Java高级开发工程师,CSDN博客专家,后端领域优质创作者
📕系列专栏:多线程及高并发系列
📕其他专栏:微服务框架系列、MySQL系列、Redis系列、Leetcode算法系列、GraphQL系列
📜如果感觉博主的文章还不错的话,请👍点赞收藏关注👍支持一下博主哦❤️
✨时间是条环形跑道,万物终将归零,亦得以圆全完美

volatile & synchorized 详解

    • volatile
      • 保证线程可见性
      • 禁止指令重排序
      • 内存屏障
      • volatile 使用优化
    • synchronized
      • 原理分析
        • 加解锁原理
        • 可重入原理
        • 保证可见性原理
      • 锁位置
      • 锁状态
        • 偏向锁
          • 偏向锁撤销
        • 轻量级锁
          • 轻量级锁的释放
        • 重量级锁
      • 锁升级


多线程及高并发系列

  • 【多线程及高并发 一】内存模型及理论基础
  • 【多线程及高并发 二】线程基础及线程中断同步
  • 【多线程及高并发 番外篇】虚拟线程怎么被 synchronized 阻塞了?

volatile

volatile 关键字的主要作用是保证被修饰变量的可见性禁止指令重排序,从而在多线程环境下确保数据的一致性和正确性

  1. 保证线程可见性volatile 关键字保证了被修饰变量的可见性。当一个线程修改了被 volatile 修饰的变量的值时,该值会立即被写入主内存,并且其他线程在读取该变量时会从主内存中获取最新值,而不是使用自己线程的缓存值。这样可以确保不同线程之间对该变量的修改是可见的,避免了由于缓存不一致导致的数据不一致性问题。
  2. 禁止指令重排序volatile 关键字还具有禁止指令重排序的效果。编译器和处理器在对指令进行优化时,可能会对指令的执行顺序进行一定的调整,但是对于被 volatile 修饰的变量,编译器和处理器会遵循特定的规则,确保指令不会被重排序,保证了变量赋值操作的有序性

volatile 关键字解决的是可见性和指令重排序问题,但不能保证原子性。如果需要实现线程安全的原子操作,可以考虑使用synchronized关键字或者java.util.concurrent.atomic包中提供的原子类

volatile 修饰的变量,汇编代码会多执行lock addl $0x0,(%rsp)操作,相当于内存屏障。lock指令的底层实现:如果支持缓存行会加缓存锁(MESI);如果不支持缓存锁,会加总线锁

保证线程可见性

public class TestVolatile {
    private static boolean stop = false;

    public static void main(String[] args) {
        new Thread("Thread A") {
            @Override
            public void run() {
                while (!stop) {
                }
                System.out.println(Thread.currentThread() + " stopped");
            }
        }.start();

        // Thread-main
        try {
            TimeUnit.SECONDS.sleep(1);
            System.out.println(Thread.currentThread() + " after 1 seconds");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        stop = true;
    }
}

stop变量没有使用volatile修饰,在主线程里的赋值无法被其他线程所“看到”,没能保证线程间的可视性

volatile 保证线程可见性,底层依赖硬件,即CPU缓存一致性协议实现,常见的是MESI协议

禁止指令重排序

标准的单例模式也会使用 volatile 关键字以禁止指令重排序,以免发生线程不安全的问题

public class Singleton {
    /**
     * volatile保证多个线程不缓存此变量
     */
    private volatile static Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

使用 volatile 关键字是为了防止singleton = new Singleton();这一步被指令重排序

实际上,创建对象这一步分为三个子步骤:

  1. 分配对象的内存空间
  2. 初始化对象
  3. singleton变量指向分配的内存空间

当步骤2和步骤3被重排序,顺序从1-2-3变为1-3-2,在多线程情况下可能会出现返回一个未初始化的对象。因为需要使用 volatile 关键字防止指令重排序

内存屏障

为了保证内存可见性,Java 编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。详见【多线程及高并发 一】内存模型及理论基础

为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能。为此,JMM采取保守策略。下面是基于保守策略的JMM内存屏障插入策略。

  1. 在每个 volatile 写操作的前面插入一个StoreStore屏障
  2. 在每个 volatile 写操作的后面插入一个StoreLoad屏障
  3. 在每个 volatile 读操作的后面插入一个LoadLoad屏障
  4. 在每个 volatile 读操作的后面插入一个LoadStore屏障

在实际执行时,只要不改变 volatile 写/读的内存语义,编译器可以根据具体情况省略不必要的屏障

volatile 使用优化

缓存行(cache line) 是缓存中可以分配的最小存储单位。缓存行是内存和 CPU 缓存之间的数据传输单位,通常为 64 字节(具体大小可能与硬件和架构相关)

著名的Java并发编程大师 Doug lea 在 JDK 7 的并发包里新增一个队列集合类LinkedTransferQueue,它在使用 volatile 变量时,用一种追加字节的方式来优化队列出队和入队的性能

/** 队列中的头部节点 */
private transient final PaddedAtomicReference<QNode> head;
/** 队列中的尾部节点 */ 
private transient final PaddedAtomicReference<QNode> tail;

static final class PaddedAtomicReference <T> extends AtomicReference T> { 
    // 使用很多4个字节的引用追加到64个字节 
    Object p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, pa, pb, pc, pd, pe; 
    PaddedAtomicReference(T r) { super(r); } 
} 
public class AtomicReference <V> implements java.io.Serializable { 
    private volatile V value; 
    // code 
}

Doug lea 使用追加到 64 字节的方式来填满高速缓冲区的缓存行,避免头节点和尾节点加载到同一个缓存行,使头、尾节点在修改时不会互相锁定

synchronized

Java 中的synchronized关键字用于实现线程同步,确保在同一时间只有一个线程可以进入被 synchronized 修饰的方法或代码块,从而防止多个线程同时访问共享资源,避免出现数据竞争和并发访问的问题。

  1. 修饰实例方法,为当前实例加锁,进入同步方法前要获得当前实例的锁
  2. 修饰静态方法,为当前类对象加锁,进入同步方法前要获得当前类对象的锁
  3. 修饰代码块,指定加锁对象,对给定对象加锁,进入同步代码块前要获得给定对象的锁
// 关键字在实例方法上,锁为当前实例
public synchronized void instanceLock() {
    // code
}

// 关键字在静态方法上,锁为当前Class对象
public static synchronized void classLock() {
    // code
}

// 关键字在代码块上,锁为括号里面的对象
public void blockLock() {
    Object o = new Object();
    synchronized (o) {
        // code
    }
}

synchronized关键字在JDK 1.5 前本质上是一把悲观锁。JDK 1.5 之后进行了优化,引入了锁升级的概念。在多线程竞争不激烈的情况下,锁会从无锁状态逐渐升级为偏向锁、轻量级锁,最后升级为重量级锁,以提高性能

值得注意的是,在使用虚拟线程时,synchronized会导致虚拟线程及其作为载体的平台线程同时被阻塞。详细分析可见 【多线程及高并发 番外篇】虚拟线程怎么被 synchronized 阻塞了?

原理分析

加解锁原理

synchronized关键字在同步代码块前后加入了monitorentermonitorexit这两个指令。

  • monitorenter指令会获取锁对象,如果获取到了锁对象,就将锁计数器加1,未获取到则会阻塞当前线程。
  • monitorexit指令会释放锁对象,同时将锁计数器减1


该图可以看出,任意线程对Object的访问,首先要获得Object的监视器,如果获取失败,该线程就进入同步状态,线程状态变为BLOCKED,当Object的监视器占有者释放后,在同步队列中得线程就会有机会重新获取该监视器

可重入原理

可重入性是指当一个线程已经持有某个锁时,它可以再次获取同一个锁,而不会因为自己已经持有锁而被阻塞。在其他线程看来,这个锁仍然是被持有的状态

synchronized 关键字在 Java 中的可重入性(Reentrant)是基于加解锁原理的计数器累加递减实现的。这种可重入的机制确保了线程在递归调用或者多层嵌套的 synchronized 块中可以反复获取锁而不会造成死锁

public class ReentrantExample {
    public synchronized void outer() {
        System.out.println("外部方法");
        inner();
    }

    public synchronized void inner() {
        System.out.println("内部方法");
    }
}

可重入性只适用于同一个线程对同一个对象的锁的重入,不同线程之间的锁重入是不允许的

保证可见性原理

synchronized关键字保证可见性的原理,是基于内存模型及happens-before规则实现的。详细可见【多线程及高并发 一】内存模型及理论基础 中happens-before的监视器锁规则

public class MonitorDemo {
    private int a = 0;

    public synchronized void writer() {     // 1
        a++;                                // 2
    }                                       // 3

    public synchronized void reader() {    // 4
        int i = a;                         // 5
    }                                      // 6
}

  • 黑色的是程序顺序规则
  • 红色的是监视器锁规则:线程A释放锁happens-before线程B加锁
  • 蓝色的是传递规则

程序顺序规则:2 happens-before 3,4 happens-before 5;监视器规则:3 happens-before 4;传递规则:2 happens-before 5

锁位置

synchronized 用的锁是存在 Java 对象头里的。 如果是非数组类型,则用 2 个字宽来存储对象头,如果是数组,则会用 3 个字宽来存储对象头。在 32 位处理器中,一个字宽是 32 位;在 64 位虚拟机中,一个字宽是 64 位。

长度内容说明
32/64bitMark Word存储对象的 hashCode、分代年龄、锁标记位等
32/64bitClass Metadata Address存储到对象类型数据的指针
32/64bitArray length数组的长度(如果是数组)

Mark Word 的格式:

锁状态29 bit 或 61 bit1 bit 是否是偏向锁2 bit 锁标志位
无锁001
偏向锁线程 ID101
轻量级锁指向栈中锁记录(Lock Record)的指针无作用00
重量级锁指向互斥量(堆中的 monitor(监视器)对象)的指针无作用10
GC 标记无作用11

在 Java 中,监视器(monitor)是一种同步工具,用于保护共享数据,避免多线程并发访问导致数据不一致。在 Java 中,每个对象都有一个内置的监视器

锁状态

在JDK 1.6后,synchronied同步锁一共有四种状态:无锁、偏向锁、轻量级锁、重量级锁,它会随着竞争情况逐渐升级。锁可以升级但是不可以降级,目的是为了提供获取锁和释放锁的效率

优点缺点适用场景
偏向锁加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距如果线程间存在锁竞争,会带来额外的锁撤销的消耗适用于只有一个线程访问同步块场景
轻量级锁竞争的线程不会阻塞,提高了程序的响应速度如果始终得不到锁竞争的线程使用自旋会消耗 CPU追求响应时间。同步块执行速度非常快
重量级锁线程竞争不使用自旋,不会消耗 CPU线程阻塞,响应时间缓慢追求吞吐量。同步块执行时间较长
偏向锁

Hotspot 的作者经过以往的研究发现大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,于是引入了偏向锁

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁。只需要简单的测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果成功,表示线程已经获取到了锁。

偏向锁撤销

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。

偏向锁升级成轻量级锁时,会暂停拥有偏向锁的线程,重置偏向锁标识,这个过程看起来容易,实则开销还是很大的,大概的过程如下:

  1. 在一个安全点(在这个时间点上没有字节码正在执行)停止拥有锁的线程。
  2. 遍历线程栈,如果存在锁记录的话,需要修复锁记录和 Mark Word,使其变成无锁状态。
  3. 唤醒被停止的线程,将当前锁升级成轻量级锁。

如果应用程序里所有的锁通常处于竞争状态,那么偏向锁就会是一种累赘,对于这种情况,我们可以一开始就把偏向锁这个默认功能给关闭:

-XX:UseBiasedLocking=false
轻量级锁

轻量级锁是在大多数情况下同步块并不会有竞争出现提出的一种优化。它可以减少重量级锁对线程的阻塞带来的线程开销。从而提高并发性能

线程在执行同步块之前,JVM 会为每个线程在当前线程的栈帧中创建用于存储锁记录的空间,我们称为Displaced Mark Word。如果一个线程获得锁的时候发现是轻量级锁,会把锁的 Mark Word 复制到自己的Displaced Mark Word里面。

然后线程尝试用 CAS 将锁的 Mark Word 替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示 Mark Word 已经被替换成了其他线程的锁记录,说明在与其它线程竞争锁,当前线程就尝试使用自旋来获取锁

轻量级锁的释放

在释放锁时,当前线程会使用 CAS 操作将 Displaced Mark Word 的内容复制回锁的 Mark Word 里面。如果没有发生竞争,那么这个复制的操作会成功。如果有其他线程因为自旋多次导致轻量级锁升级成了重量级锁,那么 CAS 操作会失败,此时会释放锁并唤醒被阻塞的线程

重量级锁

重量级锁是通过操作系统的互斥机制来实现的。如果锁已经被其他线程持有,那么该线程会被阻塞,并且操作系统会将其置于等待状态。只有当锁被释放时,操作系统才会唤醒等待的线程,使其能够继续执行

锁升级

锁升级的目的是在无竞争和低竞争情况下提高性能,减少不必要的线程切换和阻塞。当竞争激烈时,锁会升级为重量级锁,以保证线程安全性

当一个线程进入 synchronized 块时,锁会按照以下升级过程进行优化:

  1. 偏向锁:如果只有一个线程访问同步块,那么锁会升级为偏向锁。这个线程可以直接获取锁,无需竞争,提高性能。
  2. 轻量级锁:当多个线程竞争同一个锁时,偏向锁会撤销,升级为轻量级锁。线程会使用乐观锁策略尝试获取锁,避免使用系统调用,提高性能。
  3. 重量级锁:如果轻量级锁获取失败,说明有其他线程竞争锁,锁会膨胀为重量级锁。这时会使用操作系统的互斥锁机制,确保线程的互斥访问,但性能较差。

锁升级

锁升级的过程是自动进行的,并且由JVM进行控制和优化,开发者无需显式干预


参考资料:

  1. Java 并发编程实战
  2. 【多线程及高并发 一】内存模型及理论基础
  3. 关键字: synchronized详解
  4. Java并发控制机制
  5. ReentrantLock与synchronized

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

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

相关文章

【快速全面掌握 WAMPServer】04.人生初体验

网管小贾 / sysadm.cc 我们在前面的教程中为小伙伴们详细地介绍了 WampServer 的安装方法&#xff0c;相信大家对于如何安装应该已经有了一个比较完全的掌握。 在完全掌握安装方法之后&#xff0c;我们还可以更加便捷地使用我为大家提供的一键安装批处理程序来快速搞定安装部署…

apache 文件读取命令执行(CVE-2021-41773)

漏洞描述&#xff1a; CVE-2021-41773 漏洞是在 9 月 15 日发布的 2.4.49 版中对路径规范化所做的更改引入到 Apache HTTP Server 中的。此漏洞仅影响具有“require all denied”访问权限控制配置被禁用的Apache HTTP Server 2.4.49 版本。成功的利用将使远程攻击者能够访问易…

【Mybatis】深入学习MyBatis:CRUD操作与动态SQL实战指南

&#x1f34e;个人博客&#xff1a;个人主页 &#x1f3c6;个人专栏&#xff1a; Mybatis ⛳️ 功不唐捐&#xff0c;玉汝于成 目录 前言 正文 一基本用法 1 CRUD操作 1. 增加&#xff08;Create&#xff09; 2. 查询&#xff08;Read&#xff09; 3. 更新&#x…

endpoints控制器源码解析

endpoints controller 的实现原理 本文从源码的角度分析KubeController Attachdetach相关功能的实现。 本篇kubernetes版本为v1.27.3。 kubernetes项目地址: https://github.com/kubernetes/kubernetes controller命令main入口: cmd/kube-controller-manager/controller-mana…

10分钟带你了解分布式系统的补偿机制

我们知道&#xff0c;应用系统在分布式的情况下&#xff0c;在通信时会有着一个显著的问题&#xff0c;即一个业务流程往往需要组合一组服务&#xff0c;且单单一次通信可能会经过 DNS 服务&#xff0c;网卡、交换机、路由器、负载均衡等设备&#xff0c;而这些服务于设备都不一…

在微服务中如何实现全链路的金丝雀发布?

目录 1. 什么金丝雀发布&#xff1f;它有什么用&#xff1f; 2.如何实现全链路的金丝雀发布 2.1 负载均衡模块 2.2 网关模块 2.3 服务模块 2.3.1 注册为灰色服务实例 2.3.2 设置负载均衡器 2.3.3 传递灰度发布标签 2.4 其他代码 2.4.1 其他业务代码 2.4.2 pom.xml 关…

出现频率高达80%的软件测试常见面试题合集(内附详细答案)

最近看到网上流传着各种面试经验及面试题&#xff0c;往往都是一大堆技术题目贴上去&#xff0c;但是没有答案。 为此我业余时间整理了这份软件测试基础常见的面试题及详细答案&#xff0c;望各路大牛发现不对的地方不吝赐教&#xff0c;留言即可。 01 软件测试理论部分 1.1…

SpingBoot的项目实战--模拟电商【1.首页搭建】

&#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 接下来看看由辉辉所写的关于SpringBoot电商项目的相关操作吧 目录 &#x1f973;&#x1f973;Welcome Huihuis Code World ! !&#x1f973;&#x1f973; 一.项目背景及技术点运用 …

你知道继电保护测试仪的价格是多少吗?

继电保护测试仪是电气设备检测中经常使用的检测仪器。它能准确、快速地检测到每个继电保护装置的一些潜在故障和问题&#xff0c;帮助电力检测工人锁定问题点&#xff0c;使继电保护装置能够正常工作&#xff0c;保护电力需求。继电保护测试仪贵吗&#xff1f;哪些因素影响价格…

链表:如何利用“假头,新指针,双指针”解决链表问题

Java学习面试指南&#xff1a;https://javaxiaobear.cn 链表是一种线性数据结构&#xff0c;其中的每个元素实际上是一个单独的对象&#xff0c;而所有对象都通过每个元素中的引用字段链接在一起。 链表是一种物理存储单元上非连续、非顺序的存储结构&#xff0c;其物理结构不能…

C# Winform教程(二):基础窗口程序

1、介绍 winform应用程序是一种智能客户端技术&#xff0c;我们可以使用winform应用程序帮助我们获得信息或者传输信息等。 2、常用属性 Name&#xff1a;在后台要获得前台的控件对象&#xff0c;需要使用Name属性。 Visible&#xff1a;指示一个控件是否可见、 Enable&…

基于动态窗口的航线规划

MATLAB2016b可以运行 % ------------------------------------------------------------------------- % File : DWA 算法 % Discription : Mobile Robot Motion Planning with Dynamic Window Approach % Author :Yuncheng Jiang % License : Modified BSD Software License A…

MySQL按月分片

一、按照月分片 使用场景为按照自然月来分片,每个自然月为一个分片,但是一年有12个月,是不是要有12个数据节点才行呢?并不是。例如我现在只有三个分片数据库,这样就可以1月在第一个数据分片中,2月在第二个数据分片中,3月在第三个数据分片中,当来到4月的时候,就会重新开…

echarts中给图表X轴和Y轴加单位以及给tooltip(提示框)增加单位

左边没有单位&#xff0c;右图是增加单位的效果。 1.x轴y轴设置单位 增加单位不管是x轴还是y轴都可以设置name字段&#xff0c;设置完name后效果是红色箭头效果。如果想要蓝色箭头效果可以使用x轴y轴的都有的 axisLabel 属性里面有formatter配置项&#xff0c;formatter支持字…

Python【json模块常用函数】

json模块常用函数 json模块是Python标准库中的一个内置模块&#xff0c;用于处理JSON&#xff08;JavaScript Object Notation&#xff09;格式的数据。它提供了一组函数来解析、序列化和操作JSON数据。 下面是json模块中常用的几个函数&#xff1a; .loads() 用于将JSON字…

YOLOv5-Lite 树莓派4B 15帧教程

【前言】 由于v5Lite仓库遗漏了不少历史问题&#xff0c;最大的问题是毕业后卷起来了&#xff0c;找不到时间更新。 上面是这篇博客的背景&#xff0c;那么先说下结论&#xff0c;使用 v5lite-e 模型&#xff0c;在 树莓派4B&#xff08;4G内存&#xff09; 上&#xff0c;有三…

C#高级 02异步编程

基础知识 1.什么是异步任务 包含了异步任务的各种状态的一个引用类型 1)正在运行、完成、结果、报错等 2)另有ValueTask值类型版本对于异步任务的抽象 1)开启异步任务后&#xff0c;当前线程并不会阻塞&#xff0c;而是可以去做其他事情 2)异步任务&#xff08;默认&#xff…

两张图片沿着斜对角线合并成一张图片

在图像融合领域&#xff0c;论文中的对比算法可视化&#xff0c;需要将红外图像和可见光图像沿着斜对角线合并成一张图片。 红外与可见光图像举例&#xff1a; 然后做出这样的效果&#xff1a; 用Python的PIL库&#xff0c;将两张图片沿着斜对角线合并成一张图片。 from PIL …

【Python基础篇】【19.异常处理】(附案例,源码)

异常处理 异常处理常见异常elsefinallyraise获取异常信息sys.exc_info()traceback 处理异常基本原则assert断点调试两种方式Debugger窗口各图标的含义1.Show Execution Point &#xff08;Alt F10&#xff09;2.Step Over&#xff08;F8&#xff09;3.Step Into &#xff08;F…

GBASE南大通用常用错误代码

错误代码为 GBASE南大通用Server 返回给应用的错误编号&#xff0c;用于唯一的标识一个错误。错误码在 GBaseErrorCode 枚举中定义。 下表仅提供通过 GBASE南大通用数据库返回给应用的常用错误码及错误描述的参考&#xff0c; 具体错误码请参考 GBase 数据库相关手册。