并发编程 - 并发可见性,原子性,有序性 与 JMM内存模型

news2024/11/17 12:25:43

1. 并发三大特性

并发编程Bug的源头: 原子性 可见性 有序性 问题

1.1 原子性

一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行。 在 Java
中,对基本数据类型的变量的读取和赋值操作是原子性操作(64位处理器)。 不采取任何的原子性保障措施的自增操作并不是原子性的,比如i++操作。
原子性案例分析
下面例子模拟多线程累加操作
public class AtomicTest {
    private static volatile int counter = 0;

    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    //synchronized (AtomicTest.class) {
                        counter++;
                   // }
                }

            });
            thread.start();
        }

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //思考counter=?
        System.out.println(counter);

    }

}
执行结果不确定, 与预期结果不符合,存在线程安全问题
如何保证原子性?
        1.通过 synchronized 关键字保证原子性
        2.通过 Lock锁保证原子性
        3.通过 CAS保证原子性
思考:在 32 位的机器上对 long 型变量进行加减操作是否存在并发隐患?
https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html#jls-17.7

1.2 可见性

可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到 修改的值。
可见性案例分析
下面是模拟两个线程对共享变量操作的例子,用来分析线程间的可见性问题
@Slf4j
public  class VisibilityTest {

    // volatile   -> lock addl $0x0,(%rsp)
    private  boolean flag = true;
   // private volatile boolean flag = true;
    //private volatile int count;


    public  synchronized void refresh() {
        // 希望结束数据加载工作
        flag = false;
        System.out.println(Thread.currentThread().getName() + "修改flag:"+flag);
    }

    public void load() {
        System.out.println(Thread.currentThread().getName() + "开始执行.....");
        while (flag) {
            //TODO  业务逻辑:加载数据
            //shortWait(10000);
            //synchronized可以保证可见性
            //System.out.println("正在加载数据......");
           // count++;
            //添加一个内存屏障   可以保证可见性
            //UnsafeFactory.getUnsafe().storeFence();
//            try {
//                Thread.sleep(0);
//            } catch (InterruptedException e) {
//                throw new RuntimeException(e);
//            }
            //Thread.yield(); //让出cpu使用权

        }

        System.out.println(Thread.currentThread().getName() + "数据加载完成,跳出循环");
    }


    public static void main(String[] args) throws InterruptedException {
        VisibilityTest test = new VisibilityTest();


        // 线程threadA模拟数据加载场景
        Thread threadA = new Thread(() -> test.load(), "threadA");
        threadA.start();

        // 让threadA先执行一会儿后再启动线程B
        Thread.sleep(1000);

        // 线程threadB通过修改flag控制threadA的执行时间,数据加载可以结束了
        Thread threadB = new Thread(() -> test.refresh(), "threadB");
        threadB.start();

    }

当flag没有volatile修饰时,不可见,执行结果线程A跳不出循环

运行结果:threadA没有跳出循环,也就是说threadB对共享变量flag的更新操作对threadA不可见, 存在可见性问题。

思考:上面例子中为什么多线程对共享变量的操作存在可见性问题?

当flag有volatile修饰时,具有可见性,执行结果线程A可以跳循环

当flag没有volatile修饰时,但是在load()方法内的while()中输出打印语句如:System.out.println("正在加载数据......")后,,执行结果线程A还是可以跳循环,原因是println()方法内有synchronized (this),具有可见性。

当flag没有volatile修饰时,但是在load()方法内的while()中加上内存屏障,执行结果线程A也是可以跳循环,具有可见性。
public class UnsafeFactory {

    /**
     * 获取 Unsafe 对象
     * @return
     */
    public static Unsafe getUnsafe() {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            return (Unsafe) field.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * 获取字段的内存偏移量
     * @param unsafe
     * @param clazz
     * @param fieldName
     * @return
     */
    public static long getFieldOffset(Unsafe unsafe, Class clazz, String fieldName) {
        try {
            return unsafe.objectFieldOffset(clazz.getDeclaredField(fieldName));
        } catch (NoSuchFieldException e) {
            throw new Error(e);
        }
    }


}

当flag没有volatile修饰时,但是在load()方法内的while()中线程睡眠的方法:如  Thread.sleep(0);后,执行结果线程A也是可以跳循环,sleep(0)方法内部调用了内存屏障,具有可见性!

        当sleep中的时间值为0时,相当于调用了Thread.yield(); 让出cpu使用权

如何保证可见性

        1. 通过 volatile 关键字保证可见性

        2. 通过 内存屏障保证可见性

        3. 通过 synchronized 关键字保证可见性

        4. 通过 Lock锁保证可见性

1.3 有序性

        即程序执行的顺序按照代码的先后顺序执行。 为了提升性能,编译器和处理器常常会对指令做重排 序,所以存在有序性问题。
有序性案例分析
思考:下面的Java程序中x和y的最终结果是什么?
public class ReOrderTest {

    private static  int x = 0, y = 0;
    private  static  int a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException {
        int i=0;
        while (true) {
            i++;
            x = 0;
            y = 0;
            a = 0;
            b = 0;

            /**
             *  x,y的值是多少:  0,1 1,0  1,1  0,0
             */
            Thread thread1 = new Thread(new Runnable() {
                @Override
                public void run() {
                    //用于调整两个线程的执行顺序
                    shortWait(20000);
                    a = 1; //volatile 写
                    // 内存屏障StoreLoad   lock; addl $0,0(%%rsp)
                    UnsafeFactory.getUnsafe().storeFence();
                    x = b; //volatile 读

                }
            });
            Thread thread2 = new Thread(new Runnable() {
                @Override
                public void run() {
                    b = 1;
                    y = a;

                }
            });

            thread1.start();
            thread2.start();
            thread1.join();
            thread2.join();

            System.out.println("第" + i + "次(" + x + "," + y + ")");
            if (x==0&&y==0){
                break;
            }
        }
    }

    public static void shortWait(long interval){
        long start = System.nanoTime();
        long end;
        do{
            end = System.nanoTime();
        }while(start + interval >= end);
    }

}
执行结果:x,y出现了0,0的结果,程序终止。出现这种结果有可能是重排序导致的
如何保证有序性
        1.通过 volatile 关键字保证有序性
        2.通过 内存屏障保证有序性
        3.通过 synchronized关键字保证有序性
        4.通过Lock锁保证有序性

2. Java内存模型详解

在并发编程中,需要处理的两个关键问题:

        1) 多线程之间如何通信(线程之间以何种机制来交换数据)。
        2)多线程之间如何同步 (控制不同线程间操作发生的相对顺序)。
线程之间常用的通信机制有两种:共享内存和消息传递,Java采用的是共享内存模型。

2.1 Java内存模型的抽象结构

        Java线程之间的通信由Java内存模型( Java Memory Model,简称JMM )控制,JMM决定一个 线程对共享变量的写入何时对另一个线程可见。
        从抽象的角度来看, JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内 存中,每个线程都有一个私有的本地内存,本地内存中存储了共享变量的副本。 本地内存是JMM的一 个抽象概念,并不真实存在,它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。
        根据JMM的规定, 线程对共享变量的所有操作都必须在自己的本地内存中进行,不能直接从主内 存中读取
从上图看,线程A和线程B之间要通信的话,必须经历以下两个步骤:
         1)线程A把本地内存A中更新过的共享变量刷新到主内存中
        2)线程B到主内存中去读取线程A之前已更新过的共享变量
所以,线程A无法直接访问线程B的工作内存,线程间通信必须经过主内存。 JMM通过控制主内存与每 个线程的本地内存之间的交互,来为Java程序提供内存可见性的保证。

主内存与工作内存交互协议

关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工
作内存同步到主内存之间的实现细节, Java内存模型定义了以下八种 原子操作 来完成
lock(锁定): 作用于 主内存的变量 ,把一个变量标识为一条线程独占状态。
unlock(解锁): 作用于 主内存变量, 把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁 定。
read(读取): 作用于 主内存变量, 把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
load(载入): 作用于 工作内存的变量 ,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
use(使用): 作用于 工作内存的变量 ,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要 使用变量的值的字节码指令时将会执行这个操作。
assign(赋值): 作用于 工作内存的变量 ,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机 遇到一个给变量赋值的字节码指令时执行这个操作。
store(存储): 作用于 工作内存的变量, 把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
write(写入): 作用于 主内存的变量 它把store操作从工作内存中得到的变量的值放入主内存的变量中。

Java内存模型还规定了在执行上述八种基本操作时,必须满足如下规则:
        如果要把一个变量从主内存中复制到工作内存,就需要按顺序地执行read和load操作, 如果把变量从工作内存中同步回主内存中,就要按顺序地执行store和write操作。但 Java内存模型只要求上述操作必须按顺序执行,而 没有保证必须是连续执行。
        不允许read和load、store和write操作之一单独出现。
        不允许一个线程丢弃它的最近assign的操作,即 变量在工作内存中改变了之后必须同步到主内存中。
        不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
        一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
        一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行 lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现
        如果对一个变量执行lock操作,将会清空工作内存中此变量的值 ,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
        如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
        对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)

可见性案例深入分析

        重点:结合可见性案例理解主内存和工作内存的交互过程

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

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

相关文章

Linux中进程的控制(上)

对于进程控制的第一个学习部分那就是使用fork去创建子进程这一部分&#xff0c;请去复习fork那一节的笔记。 这里我们主要学习一个在使用fork创建子进程的时候&#xff0c;是如何进行写时拷贝的&#xff0c;在之前的那一节fork的学习中我们学习到的是使用fork创建一个子进程&a…

企业管理系统有哪些?

文章目录 企业管理系统一、ERP 企业资源计划&#xff08;Enterprise Resource Planning&#xff09;二、OMS 订单管理系统&#xff08;Order Management System&#xff09;三、WMS 仓库管理系统&#xff08;Warehouse Management System &#xff09;四、TMS 运输管理系统 (Tr…

【计算机网络】认识协议

目录 一、应用层二、协议三、序列化和反序列化 一、应用层 之前的socket编程&#xff0c;都是在通过系统调用层面&#xff0c;如今我们来向上打通计算机网络。认识应用层的协议和序列化与反序列化 我们程序员写的一个个解决我们实际问题, 满足我们日常需求的网络程序, 都是在应…

GCC编译器 gcc编译过程 ‘ ‘ ‘ ‘ --- 记一次查缺补漏 ‘ ‘

文章目录 前言GCC介绍GCC编译过程预处理编译汇编链接关于链接&#xff1a; 前言 学习的过程遇到了.s后缀的文件&#xff0c;原来是gcc的编译过程&#xff0c;复习一下。 又牵扯到了各种C编译器&#xff0c;诸如MSVC、MinGW、ClangLLVM等&#xff0c;挖个坑先。 还有关于动态链…

大厂面试题-JVM为什么使用元空间替换了永久代?

目录 面试解析 问题答案 面试解析 我们都知道Java8以及以后的版本中&#xff0c;JVM运行时数据区的结构都在慢慢调整和优化。但实际上这些变化&#xff0c;对于业务开发的小伙伴来说&#xff0c;没有任何影响。 因此我可以说&#xff0c;99%的人都回答不出这个问题。 但是…

AI类APP上线需要注意的问题

上线AI类应用程序需要考虑一系列重要问题&#xff0c;以确保应用程序的顺利运行、用户满意度和法规遵从。以下是在上线AI应用程序时需要注意的关键问题&#xff0c;希望对大家有所帮助。北京木奇移动技术有限公司&#xff0c;专业的软件外包开发公司&#xff0c;欢迎交流合作。…

目标识别、目标追踪等计算机视觉技术在视频监控领域的应用

随着科技的不断进步和发展&#xff0c;人们的科技意识也在不断提高&#xff0c;人工智能技术也在逐渐改变着人类的生产和生活方式&#xff0c;尤其是在安防监控领域&#xff0c;人工智能技术的落地应用越来越多。 计算机视觉技术是指设备能够“看到”它正在进行的操作&#xf…

【python画画】蘑菇云爱心

来源于网上短视频 数学原理不懂&#xff0c;图个乐 import math from turtle import *def x(i):return 15 * math.sin(i) ** 3 * 20def y(i):return 20 * (12 * math.cos(i) - 5 * math.cos(2 * i) - 2 * math.cos(4 * i))speed(0) color(red) pensize(10) for i in range(51…

安全狗安装

安装waf 关闭apache程序及httpd.exe进程; 运行cmd&#xff0c;cd进入apache/bin文件夹目录&#xff0c; 执行httpd.exe -k install -n apache2.4.39; 启动apache,启动phpstudy 安全狗安装服务名称填写apache2.4.39; 安装安全狗之后就会提示报错 网站防护 可以设备黑白名单 漏…

ChatGLM系列六:基于知识库的问答

1、安装milvus 下载milvus-standalone-docker-compose.yml并保存为docker-compose.yml wget https://github.com/milvus-io/milvus/releases/download/v2.3.2/milvus-standalone-docker-compose.yml -O docker-compose.yml运行milvus sudo docker-compose up -d2、文档预处理…

华为云1核2G2M带宽HECS云服务器价格和性能测评

华为云1核2G2M带宽HECS云服务器优惠价一年51元&#xff0c;高IO_40G系统盘&#xff0c;自带独立公网IP地址&#xff0c;可选北京、乌兰察布、上海和广州地域&#xff0c;华为云1核2G2M带宽服务器适用于小型网站、软件及应用。活动链接&#xff1a;atengyun.com/go/huawei 活动打…

在Win11上部署ChatGLM2-6B详细步骤--(下)开始部署

接上一章《在Win11上部署ChatGLM2-6B详细步骤--&#xff08;上&#xff09;准备工作》 这一节我们开始进行ChatGLM2-6B的部署 三&#xff1a;创建虚拟环境 1、找开cmd执行 conda create -n ChatGLM2-6B python3.8 2、激活ChatGLM2-6B conda activate ChatGLM2-6B 3、下载…

echarts将展示全天的数据,如一天的电费,一个停车场一天的饱和度等问题

项目场景&#xff1a; 我们的项目是通过ai识别停车场的停车数,来展示此停车场全天的饱和度,如下 问题描述 后台接口给的数据,就是这种,返回所有有停车数量的时间段,但是我们的x轴要求展示全天的数据,并且可伸缩刻度展示具体时间的停车情况 [{time:2023-10-27 08:20:20,carS…

vue的双向绑定的原理,和angular的对比

目录 前言 Vue的双向绑定用法 代码 Vue的双向绑定原理 Angular的双向绑定用法 代码 Angular的双向绑定原理 理解 图片 关于Vue的双向绑定原理和与Angular的对比&#xff0c;我们可以从以下几个方面进行深入探讨&#xff1a; 前言 双向绑定是现代前端框架的核心特性之…

【sql】sql中true,false 和 null之间and、or运算的理解。

select true and null "tan",false and null "fan",true or null "ton",false or null "fon";结果如下&#xff1a; 怎么理解呢&#xff1f; 很简单&#xff0c;把null当做介于true和false中间的值&#xff0c;也就是如果true1,false…

npm : 无法加载文件 C:\Program Files\nodejs\npm.ps1,因为在此系统上禁止运行脚本。

1、在vscode终端执行 get-ExecutionPolicy &#xff0c;显示Restricted&#xff0c;说明状态是禁止的。 2、更改状态: set-ExecutionPolicy RemoteSigned 出现需要管理员权限提示&#xff0c;可选择执行 Set-ExecutionPolicy -Scope CurrentUser 出现的ExecutionPolicy参数后输…

《ATTCK视角下的红蓝对抗实战指南》一本书构建完整攻防知识体系

一. 网络安全现状趋势分析 根据中国互联网络信息中心&#xff08;CNNIC&#xff09;发布的第51次《中国互联网络发展状况统计报告》&#xff0c;截至2022年12月&#xff0c;我国网民规模为10.67亿&#xff0c;互联网普及率达75.6%。我国有潜力建设全球规模最大、应用渗透最强的…

【AI视野·今日NLP 自然语言处理论文速览 第五十七期】Wed, 18 Oct 2023

AI视野今日CS.NLP 自然语言处理论文速览 Wed, 18 Oct 2023 Totally 82 papers &#x1f449;上期速览✈更多精彩请移步主页 Daily Computation and Language Papers VeRA: Vector-based Random Matrix Adaptation Authors Dawid Jan Kopiczko, Tijmen Blankevoort, Yuki Marku…

使用 RAG、Langchain 和 Streamlit 制作用于文档问答的 AI 聊天机器人

在这篇文章中&#xff0c;我们将探索创建一个简单但有效的聊天机器人&#xff0c;该机器人根据上传的 PDF 或文本文件的内容响应查询。该聊天机器人使用 Langchain、FAISS 和 OpenAI 的 GPT-4 构建&#xff0c;将为文档查询提供友好的界面&#xff0c;同时保持对话上下文完整。…

EtherCAT转Profinet协议网关使西门子和倍福的PLC实现通讯的方法

通过倍福PLC协议&#xff0c;远创智控YC-ECTM-PN网关能与倍福系列的PLC进行通信&#xff0c;一起探索下如何操作吧。 1.网关通过网线和倍福 PLC、电脑进行连接&#xff0c;如果网口不够&#xff0c;可以使用交换机连接。 开VS软件&#xff0c;新建一个工程&#xff0c;选择对应…