volatile、ReentrantLock和synchronized保证线程可见性原理

news2025/1/11 20:05:12

主存、工作内存

        在了解什么是线程可见性前,我们先来简单了解下 Java内存模型主存工作内存抽象概念

主存:        存储的是一些共享资源的存储位置(例如静态变量等)

工作内存: 每个线程对应的栈内存对应的私有局部资源的存储位置

我们来分析一个小案例:

static boolean run = true;
 public static void main(String[] args) throws InterruptedException {
     Thread t = new Thread(()->{
         while(run){
             // 。。。
         }
     });
     t.start();

     sleep(1);
     run = false; // 线程t不会如预想的停下来
}

 为什么会有这种情况呢?让我们结合 Java内存模型 来分析下底层的原理:

1. 初始状态, t 线程刚开始从主内存读取了 run 的值到工作内存。
2. 因为 t 线程要频繁从主内存中读取 run 的值, JIT 编译器 会将 run 的值缓存至自己工作内存中的 高速缓存 中, 减少对主存中 run 的访问,提高效率
3. 1 秒之后, main 线程修改了 run 的值,并同步至主存,而 t 是从自己工作内存中的高速缓存中读取这个变量的值,结果永远是旧值

什么是线程可见性?

        内存可见性(memory visibility)——是指当某个线程正在使用对象状态而另一个线程在同时修改该状态,需要确保当一个线程修改了状态后,其他线程能够立即看到发生的状态变化。

        由于线程之间的交互都发生在主存中,但对于变量的修改又发生在自己的工作内存中,经常会造成读写共享变量的错误,我们也叫可见性错误。

可见性错误

        指当读操作与写操作在不同的线程中执行时,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情。


解决方案:

        针对前面的代码例子讲解,我们发现,之所以会导致可见性问题是因为一个线程在自己的工作内存中更新了该共享变量的副本,但是没有同步到主存中,而在另一个线程中也没有在主存中拉去最新的状态来进行刷新自己工作内存中的共享变量的副本,因此我们的解决方案就是围绕着 “修改了共享资源的线程要将自己的更改刷新到主存中,并且让该共享资源在其他线程的工作内存中失效,强制要求其拉去主存中的最新状态来实现同步”。对此,有以下三种主要的解决方式:volatile关键字ReentrantLockSynchronized

volatile关键字

因此,正对共享的资源,我们通过添加 volatile关键字 就可以确保线程间的一致性

volatile static boolean run = true;
 public static void main(String[] args) throws InterruptedException {
     Thread t = new Thread(()->{
         while(run){
             // 。。。
         }
     });
     t.start();

     sleep(1);
     run = false; // 线程t会如预想的停下来
}

volatile保证可见性的原理

        当JVM将.class文件编译为具体的CPU执行指令(也就是机器码)后,观察这些指令,我们会发现只要是加了 volatile修饰的共享变量 ,都会在指令前面加上一个以lock为前缀的指令,lock 前缀的指令会引发两件事情:

  1. 将当前处理器缓存行(工作内存)的数据写回到系统内存(主存)
  2. 一个处理器的缓存回写到内存会导致其他处理器的缓存(工作内存)失效,要拉取最新

        第一点的实现:lock信号一般不锁总线,而是锁缓存,毕竟锁总线开销的比较大。如果访问的内存区域没有缓存在处理器内部,它会锁定这块内存区域的缓存并回写到内存,并使用缓存一致性机制来确保修改的原子性,此操作被称为“缓存锁定”,缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据。

       第二点的实现:IA-32 CPU和 Intel 64 CPU使用 MESI(修改、独占、共享、无效)控制协议去维护内部缓存和其他处理器缓存的一致性,避免在总线加lock锁。CPU使用嗅探技术保证它的内部缓存、系统内存和其他处理器的缓存的数据在总线上保持一致。具体解决思路为:

  当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,那么他会发出信号通知其他CPU将该变量的缓存行设置为无效状态。当其他CPU使用这个变量时,首先会去嗅探是否有对该变量更改的信号,当发现这个变量的缓存行已经无效时,会重新从内存中读取这个变量


Synchronized

        针对可见性问题,我们也可以通过Synchronized来进行解决,代码如下:

// 易变
static boolean run = true;

// 锁对象
final static Object Lock = new Object();

public static void main(Stringl] args) throws InterruptedException {
    Thread t = new Thread(()->{
        while(true){
            // 。。。。
            synchronized (Lock) {
                if(!run) {break;}
            }
        }
    });
    t.start();
    
    sleep(1);
    log.debug("停止 t");
    synchronized (lock){
        run = false;
    } 
}

Synchronized保证可见性的原理

如果线程A要和线程B通讯(发送数据),需要经过两个步骤

        首先A需要将副本写到主内存中去,B再去主内存中读取数据,就可以读取到A更新过的共享变量了。这个过程是由我们JVM去控制的。主内存是A和B沟通的桥梁

        JVM正是通过控制主内存与每个线程的本地内存之间的交互来为我们java程序员提供内存可见性的保证。 

        在了解了java内存模型之后呢,我们来学习下Synchronized是如何做到可见性的实现的?

在释放锁之前一定会将数据写回主内存:

        一旦一个代码块或者方法被Synchronized所修饰,那么它执行完毕之后,被锁住的对象所做的任何修改都要在释放之前,从线程内存写回到主内存。也就是说他不会存在线程内存和主内存内容不一致的情况。

在获取锁之后一定从主内存中读取数据:

        同样的,线程在进入代码块得到锁之后,被锁定的对象的数据也是直接从主内存中读取出来的,由于上一个线程在释放的时候会把修改好的内容回写到主内存,所以线程从主内存中读取到数据一定是最新的
        就是通过这样的原理,Synchronized关键字保证了我们每一次的执行都是可靠的,它保证了可见性。


Reentrantlock

        synchronized 和 ReentrantLock(包括AQS的其他Lock),都能保证线程间的可见性,但实现方式有区别,以下是Reentrantlock保证线程可见性的示例代码:

public class Main {
    private static boolean run = true;
    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while (true) {
                lock.lock();
                try {
                    if (!run) {
                        break;
                    }
                    // 其他操作...
                } finally {
                    lock.unlock();
                }
            }
        });
        t.start();

        Thread.sleep(1);
        lock.lock();
        try {
            run = false;
        } finally {
            lock.unlock();
        }
    }
}

ReentrantLock保证可见性的原理

        在 lock.lock() lock.unlock() 时,都会操作 AbstractQueuedSynchronizer类 中的一个变量 state,这个变量是 volatile 修饰的,volatile变量的语句对应的汇编码指令中会多加一行lock addl $0x0, (%esp),这一行的作用是:

(1)将工作内存修改了的缓存(不仅仅是该变量的缓存)都强制刷新回主内存

(2)把其他CPU对应缓存行标记为invalid状态,那么在读取这一部分缓存时,必须回主内存读取。这样也就保证了线程间的可见性

        具体来说,当一个线程获取ReentrantLock锁时,它会将自己工作内存中的数据刷新到主内存中,这样其他线程就能够看到最新的值。而当一个线程释放ReentrantLock锁时,它会将主内存中的数据刷新到自己的工作内存中,这样其他线程就能够读取到最新的值。

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

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

相关文章

JAVASE 游戏

朴素的拼图游戏,通过WASD移动,我自己都不太会,可以通过篡改内部代码来取得想要的成功彩蛋 代码: import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.KeyEvent; import java.awt.…

如何使用autotools制作Makefile

本文将通过一个例子介绍如何使用autotools制作Makefile,最终运行可执行文件。 本例子中使用的源代码a.c如下。 #include "stdio.h" void main() {int a,b;int c;printf("请输入两个数:\n");scanf("%d %d",&a,&b)…

大数据课程L3——网站流量项目的系统搭建

文章作者邮箱:yugongshiye@sina.cn 地址:广东惠州 ▲ 本章节目的 ⚪ 了解网站流量项目的运行环境; ⚪ 了解网站流量项目的日志采集系统搭建; ⚪ 了解网站流量项目的离线业务系统搭建; ⚪ 了解网站流量项目的Hive做离线数据处理; ⚪ 了解网站流量项目的…

盲打键盘的正确指法指南

简介 很多打字初学者,并不了解打字的正确指法规范,很容易出现只用两根手指交替按压键盘的“二指禅”情况。虽然这样也能实现打字,但是效率极低。本文将简单介绍盲打键盘的正确指法,以便大家在后续的学习和工作中能够提高工作效率…

Nacos安装及在项目中的使用

目录 概要一、安装 Nacos1、下载 Nacos2、解压3、启动 Nacos 服务器4、自定义Nacos启动脚本5、访问Nacos Web控制台 二、Nacos----服务注册与发现1、添加 Nacos 依赖2、配置 Nacos 服务器地址3、使用 Nacos 注册服务4、启动服务 三、Nacos----配置管理1、创建配置数据2、从 Nac…

技师学院物联网实训室建建设方案

一、概述 1.1专业背景 物联网(Internet of Things)被称为继计算机、互联网之后世界信息产业第三次浪潮,它并非一个全新的技术领域,而是现代信息技术发展到一定阶段后出现的一种聚合性应用与技术提升,是随着传感网、通…

深圳站WOT全球技术创新大会2023,精彩即将开启!

前几天刷屏的“中国大模型顶流群聊笔记”想必很多人都看到了,包括百川智能创始人&CEO王小川,创新工场董事长、零一万物创始人李开复,澜舟科技CEO周明在内的20多位中国大模型领域模型层、工具层、应用层的“顶流”们,在西溪湿地…

【校招VIP】专业课考点之死锁

考点介绍: 在两个或者多个并发进程中,如果每个进程持有某种资源而又等待其它进程释放它或它们现在保持着的资源,在未改变这种状态之前都不能向前推进,称这一组进程产生了死锁。通俗的讲就是两个或多个进程无限期的阻塞、相互等待的…

使用Python和BeautifulSoup提取网页数据的实用技巧

在数据驱动的时代,获取网页数据并进行分析和处理是一项重要的任务。Python作为一门强大的编程语言,在处理网页数据的领域也表现出色。本文将分享使用Python和BeautifulSoup库提取网页数据的实用技巧,帮助你更高效地获取和处理网页数据。 1、了…

什么牌子的运动蓝牙耳机好用、运动耳机品牌排行榜推荐

运动与健康息息相关,人们越来越认识到运动带来的益处,例如增强心肺功能、提高新陈代谢率,并能预防多种慢性疾病。随着社交媒体上越来越多人分享自己的运动心得,更多的人也被激发起参与其中的热情。 在运动过程中,音乐的…

猫头虎博主赠书三期:《Go编程进阶实战: 开发命令行应用、HTTP应用和gRPC应用》

🎉猫头虎博主赠书三期🎉:《Go编程进阶实战: 开发命令行应用、HTTP应用和gRPC应用》 🌷🍁 博主猫头虎(🐅🐾)带您 Go to New World✨🍁 🦄 博客首页…

EditPlus 配置python 及Anaconda中的python

若不是pycharm vscode 太大,太占内存,谁会想到用Notepad,EdirPlus 配置python呢!!! 话不多说,首先你自己安装好EditPlus。开始 菜单栏 选择 工具 -> 配置自定义工具 组名:python 命令:d:\*…

基于51单片机的称重电子秤proteus仿真设计

一、设计背景 随着微电子技术的应用,市场上使用的传统称重工具已经满足不了人们的要求。为了改变传统称重工具在使用上存在的问题,在本设计中将智能化、自动化、人性化用在了电子秤重的控制系统中。本系统主要由单片机来控制,测量物体重量部分使用称重传…

雅思口语同替高分表达

目录 雅思口语题目准备 Riding a bike 1. Did you have a bike when you were young? 2. Did you ride a bike when you were little? 3. Did you ride a bike to school? 4. Do you ride a bike when you go out no? 雅思口语经验 口语高分同替词汇 中式英语问题…

计算机竞赛 基于生成对抗网络的照片上色动态算法设计与实现 - 深度学习 opencv python

文章目录 1 前言1 课题背景2 GAN(生成对抗网络)2.1 简介2.2 基本原理 3 DeOldify 框架4 First Order Motion Model5 最后 1 前言 🔥 优质竞赛项目系列,今天要分享的是 🚩 基于生成对抗网络的照片上色动态算法设计与实现 该项目较为新颖&am…

系列五、Nginx配置实例之反向代理1

一、目标 Linux服务器部署Tomcat的运行环境,然后通过Windows的www.123.com访问,能够访问到Tomcat的主页(有猫的页面) 二、步骤 2.1、下载apache-tomcat-8.5.63.tar.gz安装包 # 我分享的 链接:https://pan.baidu.com…

【数据结构与算法系列4】长度最小的子数组 (C++ Python)

给定一个含有 n 个正整数的数组和一个正整数 target 。 找出该数组中满足其总和大于等于 target 的长度最小的 连续子数组 [numsl, numsl1, ..., numsr-1, numsr] ,并返回其长度**。**如果不存在符合条件的子数组,返回 0 。 示例 1: 输入&…

virtualbox虚拟机中安装FreeDOS系统和DJGPP编译环境

一、安装FreeDOS系统 1、从官网下载FreeDOS系统镜像,下载的压缩包中包含两个文件:后缀为.iso和.img的镜像 ​​​下载页面 http://www.freedos.org/download/ 直接下载链接 https://www.ibiblio.org/pub/micro/pc-stuff/freedos/files/distributions/1.…

TAITherm专业热管理工具

TAITherm是ThermoAnalytics公司开发的专业三维热仿真分析工具,广泛应用于国内外汽车、工业自动化、轨道交通、重型机械等行业的热仿真设计中。同系列的CoTherm耦合优化平台可支持热流耦合、一三维耦合、FMU集成、设计优化、敏感性分析等应用。 产品模块介绍 TAITh…

LeetCode518. 零钱兑换 II 以及 动态规划相关的排列组合问题

文章目录 一、题目二、题解方法一:完全背包问题的变体(版本1)方法二:完全背包问题变体(版本2) 三、拓展:先遍历物品后遍历背包vs先遍历背包后遍历物品先遍历物品后遍历背包(组合问题…