java并发编程之 volatile关键字

news2024/9/25 21:25:56
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则是直接锁住当前变量,只有当前线程可以访问,其他线程阻塞。

11、单例模式中,使用了双重判断,为什么还需要volatile关键字?

synchronized关键字可以保证原子性和可见性,但是没有办法保证顺序性(即可以被指令重排序),new 一个对象可以分为以下三个步骤:

  1. 为对象分配内存空间
  2. 初始化对象
  3. 对象地址的引用

假设new 对象的过程发生了指令重排序,步骤2和3互换。虽然创建对象加锁了,但是加锁线程1执行完1和3之后失去了CPU时间片,此时初始化对象还没有完成,这个时候线程2执行,在外层判断的时候INSATNCE!=null, 直接就获取对象执行操作了,但是因为该对象实际还没有初始化完成呢,因此线程2返回的就是一个空对象。

而volatile关键字禁止指令重排序,就避免了上述问题。

    public static T01 getInstance(){
        private static volatile T01 INSTANCE;
        private T01 (){
        //私有构造器,外部不能new
        }
 
        if(INSTANCE==null){
            synchronized (T01.class){
                if(INSTANCE==null){
                    INSTANCE=new T01();
                }
            }
        }
        return INSTANCE;
    }

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

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

相关文章

C语言每日一题06

一、题目 二、解析 void main &#xff08;&#xff09; { char c1&#xff0c;c2&#xff1b; int a1&#xff0c;a2&#xff1b; c1 getchar &#xff08;&#xff09;&#xff1b;//读取第一个输入&#xff0c;c11 scanf &#xff08;“%3d”&#xff0c;&a1&#xff…

Python螺旋折线蓝桥杯(来源lanqiao.cn 题目176) 时间超限

题目描述 如图所示的螺旋折线经过平面上所有整点恰好一次。 对于整点(X, Y)&#xff0c;我们定义它到原点的距离dis(X, Y)是从原点到(X, Y)的螺旋折线段的长度。 例如dis(0, 1)3, dis(-2, -1)9 给出整点坐标(X, Y)&#xff0c;你能计算出dis(X, Y)吗&#xff1f; 输入格式 …

MySQL--Buffer Pool

虽然说 MySQL 的数据是存储在磁盘里的&#xff0c;但是也不能每次都从磁盘里面读取数据&#xff0c;这样性能是极差的。为此&#xff0c;Innodb 存储引擎设计了一个缓冲池&#xff08;Buffer Pool&#xff09;&#xff0c;来提高数据库的读写性能。 有了缓冲池后&#xff1a; …

Linux系统本地部署Docker Compose UI服务结合内网穿透实现公网访问

文章目录 1. 安装Docker2. 检查本地docker环境3. 安装cpolar内网穿透4. 使用固定二级子域名地址远程访问 Docker Compose UI是Docker Compose的web界面。这个项目的目标是在Docker Compose之上提供一个最小的HTTP API&#xff0c;同时保持与Docker Compose CLI的完全互操作性。…

如何在CentOS搭建docker compose ui可视化工具并无公网IP远程管理容器

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

谷歌Gemma大模型部署记录

谷歌Gemma大模型部署记录 配置信息 1.系统&#xff1a;Ubuntu20 2.显卡&#xff1a;RTX3060 6G 一、安装Ollama 官网地址&#xff1a;https://ollama.com/download/linux 按照指令安装 curl -fsSL https://ollama.com/install.sh | sh二、运行模型 输入指令&#xff1a;…

ubuntu中截圖工具

文章目录 一、flameshot1、安裝指令2、延遲截圖 二、总结 github社區 參考1 參考二 一、flameshot 1、安裝指令 sudo apt install flameshot如果想要把此時的照片復制下來&#xff0c;點擊從右數第六個圖標 2、延遲截圖 flameshot gui -d 3000二、 总结

使用腾讯云轻量应用服务器怎么搭建网站?

2024年腾讯云轻量应用服务器搭建网站教程&#xff0c;建站大全&#xff0c;包括WordPress建站、宝塔面板安装、Typecho博客搭建、LAMP、LNMP、Node.js、ASP.NET、Docker、K3s、WooCommerce、互动直播后台搭建、SRS音视频服务器、Matomo网站数据统计等网站搭建教程&#xff0c;腾…

【JS】浅谈Promise

Promise 前言一、Promise是什么&#xff1f;二、为什么用Promise&#xff1f;2.1解决回调地狱2.2 集中错误处理2.3代码解耦和复用 三、做什么&#xff1f;四、原型方法和实例方法&#xff1f;五、应用场景&#xff1f; 前言 promise是es6的新规范&#xff0c;它是一种异步解决…

【漏洞复现】6.Struts2 S2-061 远程命令执行漏洞(CVE-2020-17530)复现与分析

文章目录 1. 预备知识2. 漏洞复现2.1 漏洞介绍2.2 漏洞原理分析2.2.1 Apache Struts2架构2.2.2 OGNL语法介绍2.2.3漏洞原理 2.3 漏洞复现2.3.1 靶场搭建2.3.2 漏洞探测2.3.3 漏洞利用2.3.4 POC分析 2.4 漏洞修复 1. 预备知识 Struts是一个用于开发Java EE网络应用程序的开放源代…

2024年03月 Discourse 3.3.0.beta1 版本的更新

在这个版本的更新中 Discourse 完成了 Ember 5 版本的升级和更新。 Ember.js是一个用于创建 web 应用的 开源JavaScript MVC 框架&#xff0c;采用基于字符串的Handlebars 模板&#xff0c;支持双向绑定、观察者模式、计算属性&#xff08;依赖其他属性动态变化&#xff09;、…

扩展以太网(数据链路层)

目录 一、在物理层扩展以太网 二、在数据链路层扩展以太网 三、以太网交换机的特点 四、以太网交换机的交换方式 五、以太网交换机的自学习功能 六、小结 一、在物理层扩展以太网 使用光纤扩展&#xff1a; • 主机使用光纤&#xff08;通常是一对光纤&#xff09;和…

C语言基础知识点(十八)联合、

【C语言】联合体-共用体 &#xff08;union&#xff09; 详解-阿里云开发者社区 (aliyun.com) 联合 在C语言中是一种数据类型&#xff0c;能在同一个内存空间中存储不同的数据类型&#xff08;不是同时储存&#xff09;。 典型用法&#xff1a;设计一种表以存储及无规律、实…

Redis相关操作大全一篇全搞定

Redis是单线程吗? Redis 的单线程主要是指 Redis 的网络 10 和键值对读写是由一个线程来完成的&#xff0c;这也是 Redis 对外提供键值存储服务的主要流程。但Redis 的其他功能&#xff0c;比如持久化、异步删除、集群数据同步等&#xff0c;其实是由额外的线程执行的。 Redi…

【java】java环境变量分类

测试代码&#xff1a; public class TestSys {public static void main(String[] args) {/*** 获取所有的系统环境变量*/Map<String, String> map System.getenv();map.forEach((key, value) -> System.out.printf("env&#xff1a;key:%s->value:%s%n"…

Linux系统部署SQL Server结合内网穿透实现公网访问本地数据库

文章目录 前言1. 安装sql server2. 局域网测试连接3. 安装cpolar内网穿透4. 将sqlserver映射到公网5. 公网远程连接6.固定连接公网地址7.使用固定公网地址连接 前言 简单几步实现在Linux centos环境下安装部署sql server数据库&#xff0c;并结合cpolar内网穿透工具&#xff0…

GAT精译 - 1

2 GAT ARCHITECTURE 我们将描述一个单独的图注意力层&#xff0c;因为单层在我们实验中GAT架构。我们使用这个特殊的注意力是follow Bahdanau et al 2015的工作。 我们层的输入是节点的特征&#xff0c;&#xff0c;,N是节点的数量&#xff0c;F是每一个节点的特征数量。这个层…

STM32定时器不按设定超时产生中断

1. 引言 某客户设计需要启动定时器在 3ms 后产生中断&#xff0c;其后定时器不再运行&#xff0c;直至下一次软件要求再次启动定时器产生中断&#xff0c;实测代码后发现定时器启动后立即产生了超时中断。 2. 调研 客户通过 STM32CubeMX 配置 TIM7 并生成工程&#xff0c;在…

外包干了15天,技术退步明显。。。。。。

说一下自己的情况&#xff0c;本科生&#xff0c;19年通过校招进入武汉某软件公司&#xff0c;干了接近4年的功能测试&#xff0c;今年年初&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落!而我已经在一个企业干了四年的功能测试&a…

基于python+vue智慧农业小程序flask-django-php-nodejs

传统智慧农业采取了人工的管理方法&#xff0c;但这种管理方法存在着许多弊端&#xff0c;比如效率低下、安全性低以及信息传输的不准确等&#xff0c;同时由于智慧农业中会形成众多的个人文档和信息系统数据&#xff0c;通过人工方法对知识科普、土壤信息、水质信息、购物商城…