java并发编程之 volatile关键字

1、简单介绍一下JMM

Java 内存模型(Java Memory Model 简称JMM)是一种抽象的概念,并不真实存在,指一组规则或规范,通过这组规范定义了程序中各个变量的访问方式。java内存模型(JMM)屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果。

JMM规定:

  1. 所有的共享变量都存储在主内存中(包括实例变量、类变量,静态变量,但是不包括局部变量,因为局部变量是线程私有的,不存在多线程之间的竞争)
  2. 每个线程都有自己的工作内存,线程工作内存中保留了被线程使用的共享变量的副本
  3. 线程对变量的操作(读或者写)都必须在工作内存中完成,不能直接操作主内存
  4. 不同线程之间不能相互访问对方的工作内存,线程间变量值的传递需要通过主内存完成。

工作内存和主内存的关系图:

2、分析一下共享变量的不可见性问题。

看代码,定义了一个成员变量 falg,一个子线程负责修改flag的值,另外一个子线程根据flag的值判断是否跳出空循环,实际执行结果为下图,可见,线程0对flag的修改并没有影响到线程1;这就是多线程下共享变量的修改会存在不可见性

原因就是Thread-1一直访问的都是自己本地内存中的flag,而没有从主内存中去更新flag,所以没办法跳出循环。

public class TestVolatile {
    // 定义一个成员变量
    static boolean flag = true;

    public static void main(String[] args) {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "start");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            flag = false;
            System.out.println(Thread.currentThread().getName() + "end");
        }).start();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "start");
            while (flag) {
                // 空转
            }
            System.out.println(Thread.currentThread().getName() + "end");
        }).start();
    }
}

3、解决共享变量可见性的两种方式
  • volatile关键字
  • 加锁

3.1、volatile关键字处理,还是上述代码,只要在flag属性前加一个volatile关键字,就可以了。

3.2、 加锁处理,看代码

线程1赋值修改flag,线程2不断读取flag,可以看到flag被线程1修改后,线程2是可以读取到变化之后的结果的。

public class TestVolatile {
    // 定义一个成员变量
    static boolean flag = true;

    static final Object lock = new Object();

    public static void main(String[] args) {
        test02(lock);
    }

    private static void test02(Object lock) {
        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "start");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            flag = false;
            System.out.println(Thread.currentThread().getName() + "end");
        }).start();

        new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + "start");

            while (true) {
                synchronized (lock) {
                    if (flag) {
                        System.out.println("flag = " + flag);
                        try {
                            Thread.sleep(1000);
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    } else {
                        System.out.println("flag = " + flag);
                        break;
                    }
                }
            }

            System.out.println(Thread.currentThread().getName() + "end");
        }).start();
    }
}

执行结果

4、解决共享变量可见性的原理?
  1. 加锁:某一个线程进入synchronized代码块后,执行过程如下:线程获得锁,清空工作内存,从主内存中拷贝最新值到工作内存,执行代码,修改后的副本值刷新回主内存,线程释放锁
  2. volatile关键字:其实还是工作内存的及时刷新,volatile有以下语义
    1. 写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中
    2. 读一个volatile变量时,JMM会把该线程本地内存设为无效,重新回到主内存中读取最新共享变量。

5、volatile不保证原子性

volatile是不保证原子性操作的。

    static volatile int count = 0;

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    count++;
                }
                System.out.println(Thread.currentThread().getName() + "==========> count=" + count);
            }).start();
        }
    }

如上述代码,定义了一个volatile修饰的int类型变量,启动10个线程去执行++操作,每个线程修改10000次,按理说修改后的值应该为100000,但是每次执行的结果都没有到100000

发生上述问题的原因在于,count++这个操作不是原子性的,他包含三个步骤:

  1. 从主内存中读取数据导工作内存
  2. 对工作内存中的数据进行+1操作
  3. 将工作内存中的数据写会到主内存

假设某一时间,两个线程都执行到了步骤1,读取到的count值是100 ,然后线程1的CPU时间片到了,停止执行,此时2线程继续执行23步骤,将主内存的值修改为101,这个时候线程1继续执行,但是因为1已经执行了,没有重新去主内存中取值,因此执行23操作后,新值为101,然后往主内存修改的值也是101。

解决原子性办法,

1、加锁

    static final Object lock = new Object();

    static volatile int count = 0;

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    synchronized (lock){
                        count++;
                    }
                }
                System.out.println(Thread.currentThread().getName() + "==========> count=" + count);
            }).start();
        }
    }

2、使用atomic包 // 底层CAS,不多介绍了。

    static AtomicInteger a = new AtomicInteger();

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    a.getAndIncrement();
                }
                System.out.println(Thread.currentThread().getName() + "==========> count=" + a);
            }).start();
        }
    }

6、指令重排序介绍。

什么是重排序?为了提高性能,编译器和处理器常常会对指令进行重新排序。一般重排序分为以下三种:编译器优化重排序,指令级并行重排序,内存系统重排序。

重排序就是为了提高处理初度,如下图。

6.1、指令重排序在多线程并发下会产生什么问题?

经典案例,以下代码执行完毕,i 和 j 的值有可能是多少?经过测试,i=0,j=0的情况也会出现!这就是指令重排序导致的问题,


import java.util.concurrent.CountDownLatch;

public class T01_Disorder {
    private static int x = 0, y = 0;
    private static int a = 0, b = 0;

    public static void main(String[] args) throws InterruptedException {

        for (long i = 0; i < Long.MAX_VALUE; i++) {
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            CountDownLatch latch = new CountDownLatch(2);

            Thread one = new Thread(new Runnable() {
                public void run() {
                    a = 1;
                    x = b;

                    latch.countDown();
                }

            });

            Thread other = new Thread(new Runnable() {
                public void run() {
                    b = 1;
                    y = a;

                    latch.countDown();
                }
            });
            one.start();
            other.start();
            latch.await();
            String result = "第" + i + "次 (" + x + "," + y + ")";
            if (x == 0 && y == 0) {
                System.err.println(result);
                break;
            }
        }
    }

}

7、volatile是怎么保证指令执行顺序的?

jvm级别,识别到volatile关键词,会执行jvm内存屏障,包括 loadload 屏障、storestore 屏障、loadstore屏障、storeload 屏障(其中load是读,store是写);

a) 会在写之前加 storestore,写之后加storeload,保证在自己写之前完成其他的写,在自己写完之后才能继续其他的读

b) 会在读之后加上loadload 和 loadstore ,保证在自己读完之后其他的才能读,自己读完之后,其他的才能写

8、 Happens Before原则?

简单的说,如果 A    Happens Before B ,那么,A的操作对B,都是可见的。

 Happens Before模型是由8条具体规则组成的:

  1. 程序顺序规则:单线程中,每个操作 都  Happens Before 他后面的操作。
  2. 监视器规则:一个线程解锁, Happens Before 后面线程的加锁
  3. volatile变量规则:对一个volatile变量的写, Happens Before 对这个volatile的读
  4. 传递规则:A  Happens Before B,B  Happens Before C,则 A  Happens Before C
  5. start() 规则:如果线程A执行ThreadB.start(),那么A线程的ThreadB.start()操作 happens-before  线程B的任意操作。
  6. join() 规则:如果线程A执行ThreadB.join(),那么B线程中的任意操作 happens-before 线程A从ThreadB.join()成功返回。
  7. 程序中断规则:对线程interrupted()方法的调用 happens-before 被中断线程的代码检测到中断时间的发生;
  8. 对象finalize规则:一个对象初始化完成(构造函数执行结束)happens-before 于发生它的finalize()方法的开始

9、总结一下volatile关键字的作用
  1. 保证变量的可见性
  2. 禁止指令重排序

10、volatile和synchronized的区别?
  1. 关键字使用范围:volatile只能修饰变量,synchronized关键字能修饰变量,方法,代码块
  2. 是否会阻塞线程:volatile不会阻塞线程,synchronized会阻塞线程
  3. 原子性:volatile不保证原子性,synchronized可以保证原子性
  4. 可见性:volatile 和 synchronized 都可以保证 修改的可见性
  5. 指令重排序:volatile禁止指令重排序,synchronized允许被编译器优化。

总的来说,volatile的本质是告诉JVM,变量在工作内存(寄存器)中的值是不确定的,需要从主存中去取,synchronized则是直接锁住当前变量,只有当前线程可以访问,其他线程阻塞。

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

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

相关文章

OpenvSwitch VXLAN 隧道实验

OpenvSwitch VXLAN 隧道实验 最近在了解 openstack 网络&#xff0c;下面基于ubuntu虚拟机安装OpenvSwitch&#xff0c;测试vxlan的基本配置。 节点信息&#xff1a; 主机名IP地址OS网卡node1192.168.95.11Ubuntu 22.04ens33node2192.168.95.12Ubuntu 22.04ens33 网卡信息&…

XUbuntu22.04之关闭todesk开机自启动(二百二十一)

简介&#xff1a; CSDN博客专家&#xff0c;专注Android/Linux系统&#xff0c;分享多mic语音方案、音视频、编解码等技术&#xff0c;与大家一起成长&#xff01; 优质专栏&#xff1a;Audio工程师进阶系列【原创干货持续更新中……】&#x1f680; 优质专栏&#xff1a;多媒…

Python数据分析-4

1.对于一组电影数据&#xff0c;呈现出rating,runtime的分布情况&#xff1a; #encodingutf-8 import pandas as pd import numpy as np from matplotlib import pyplot as plt file_path "./youtube_video_data/IMDB-Movie-Data.csv" df pd.read_csv(file_path) …

React低代码平台实战:构建高效、灵活的应用新范式

文章目录 每日一句正能量前言一、React与低代码平台的结合优势二、基于React的低代码平台开发挑战三、基于React的低代码平台开发实践后记好书推荐编辑推荐内容简介作者简介目录前言为什么要写这本书 读者对象如何阅读本书 赠书活动 每日一句正能量 人生之美&#xff0c;不在争…

C# 根据两点名称,寻找两短路程的最优解,【有数据库设计,完整代码】

前言 如果我们遇到路径问题&#xff0c;可以使用点点连线&#xff0c;给定一个点&#xff0c;可以到达另外几个点&#xff0c;寻找最优解 例&#xff1a;如下图所示&#xff0c;如果要从A1-C1,可以有三条路 1.A1-B1-C1 2.A1-B2-C1 3.A1-B3-C1 最优解肯定是A1-B1-C1&#xff0c…

15. jwt认证中间件

在上一篇登录功能的实现中&#xff0c;我们使用了jwt作为鉴权组件&#xff0c;其中登录后会颁发token。前端在访问后续请求时&#xff0c;可以带上这个token。对于一些需要权限校验的请求&#xff0c;我们就需要验证这个token&#xff0c;从token中获取到用户id&#xff08;有了…

Unity Timeline学习笔记(1) - 创建TL和添加动画片段

Timeline在刚出的时候学习了一下&#xff0c;但是因为一些原因一直都没用在工作中使用。 版本也迭代了很久不用都不会用了&#xff0c;抽时间回顾和复习一下&#xff0c;做一个笔记后面可以翻出来看。 创建Timeline 首先我们创建一个场景&#xff0c;放入一个Plane地板&#…

【机器学习智能硬件开发全解】(四)—— 政安晨:嵌入式系统基本素养【后摩尔时代】

随着物联网、大数据、人工智能时代的到来&#xff0c;海量的数据分析、大量复杂的运算对CPU的算力要求越来越高。 CPU内部的大部分资源用于缓存和逻辑控制&#xff0c;适合运行具有分支跳转、逻辑复杂、数据结构不规则、递归等特点的串行程序。 在集成电路工艺制程将要达到极…

PgSQL技术内幕 - 优化器如何估算行数

PgSQL技术内幕 - 优化器如何估算行数 PgSQL优化器根据统计信息估算执行计划路径的代价&#xff0c;从而选择出最优的执行计划。而这些统计信息来自pg_statistic&#xff0c;当然这个系统表是由ANALYZE或者VACUUM进行样本采集而来。关于该系统表的介绍详见&#xff1a;PgSQL技术…

水泵房远程监控物联网系统

随着物联网技术的快速发展&#xff0c;越来越多的行业开始利用物联网技术实现设备的远程监控与管理。水泵房作为城市供水系统的重要组成部分&#xff0c;其运行状态的监控与管理至关重要。HiWoo Cloud作为专业的物联网云服务平台&#xff0c;为水泵房远程监控提供了高效、稳定、…

2.1HTML5基本结构

HTML5实际上不算是一种编程语言&#xff0c;而是一种标记语言。HTML5文件是由一系列成对出现的元素标签嵌套组合而成&#xff0c;这些标签以<元素名>的形式出现&#xff0c;用于标记文本内容的含义。浏览器通过元素标签解析文本内容并将结果显示在网页上&#xff0c;而元…

基于centos7的k8s最新版v1.29.2安装教程

k8s概述 Kubernetes 是一个可移植、可扩展的开源平台&#xff0c;用于管理容器化的工作负载和服务&#xff0c;可促进声明式配置和自动化。 Kubernetes 拥有一个庞大且快速增长的生态&#xff0c;其服务、支持和工具的使用范围相当广泛。 Kubernetes 这个名字源于希腊语&…

CentOS无法解析部分网站(域名)

我正在安装helm软件&#xff0c;参考官方文档&#xff0c;要求下载 get-helm-3 这个文件。 但是我执行该条命令后&#xff0c;报错 连接被拒绝&#xff1a; curl -fsSL -o get_helm.sh https://raw.githubusercontent.com/helm/helm/main/scripts/get-helm-3 # curl: (7) Fai…

深入探索Java中的MarkWord与锁优化机制——无锁、偏向锁、自旋锁、重量级锁

引言 在Java并发编程领域&#xff0c;有效管理对共享资源的访问显得尤为关键。为了保障线程安全&#xff0c;Java虚拟机&#xff08;JVM&#xff09;引入了一系列精妙的锁机制&#xff0c;这其中的核心概念就是Java对象头中的MarkWord。本文将详尽解析MarkWord的作用&#xff…

基于Centos7部署OceanBase4.2版本单副本集群

&#x1f4e2;&#x1f4e2;&#x1f4e2;&#x1f4e3;&#x1f4e3;&#x1f4e3; 哈喽&#xff01;大家好&#xff0c;我是【IT邦德】&#xff0c;江湖人称jeames007&#xff0c;10余年DBA及大数据工作经验 一位上进心十足的【大数据领域博主】&#xff01;&#x1f61c;&am…

1.绪论

目录 1.1 Web原理基础 1.1.1 Internet与万维网 1.1.2 Web架构 1.2 Web前端技术基础 1.2.1 HTML技术 1.2.2 CSS技术 1.2.3 JavaScript技术 1.3 Web前端新技术 1.3.1 HTML5技术 1.3.2 CSS3技术 1.3.3 jQuery技术 1.4 Web开发工具 1.1 Web原理基础 1.1.1 Internet与万…

Java实现PDF文字内容识别,结合OCR实现PDF图片实现

使用插件&#xff1a;UMI-OCR、PDFBOX 实现思路&#xff1a;通过PDFBOX识别PDF文字&#xff0c;如果是图片&#xff0c;则识别不出来&#xff0c;再调用OCR进行识别返回文字&#xff1b;OCR识别较慢&#xff0c;长图识别不出来&#xff0c;目前HTTP方式只支持图片格式&#xf…

2024年【流动式起重机司机】考试题及流动式起重机司机模拟考试题

题库来源&#xff1a;安全生产模拟考试一点通公众号小程序 流动式起重机司机考试题参考答案及流动式起重机司机考试试题解析是安全生产模拟考试一点通题库老师及流动式起重机司机操作证已考过的学员汇总&#xff0c;相对有效帮助流动式起重机司机模拟考试题学员顺利通过考试。…

008:安装Docker

安装Docker 如果不太熟悉Linux命令&#xff0c;不想学习Linux命令&#xff0c;可以直接看文末NAS面板章节&#xff0c;通过面板&#xff0c;像使用Window一样操作NAS。 一、安装 Docker 1.安装 Docker wget -qO- https://get.docker.com/ | sh2.启动 Docker 服务 sudo sys…

Ubuntu系统的安装及基础操作

目录 一、VMware虚拟机安装Ubuntu20.04过程 1、安装前的准备工作 2、VMware虚拟机创建Ubuntu操作系统 步骤一&#xff1a;以管理员的身份运行VMware虚拟机 步骤二&#xff1a;新建虚拟机 步骤三&#xff1a;选择类型配置 步骤四&#xff1a;选择安装客户机操作系统 步骤…