当前位置: 首页 > article >正文

Java内存模型 volatile 线程安全

目录

    • Java内存模型
      • 可见性例子和volatile
      • volatile如何保证可见性
      • 原子性与单例模式
      • i++非原子性
    • 线程安全

Java内存模型

参考学习: Java Memory Model外文文档

  • CPU与内存,可参考:https://blog.csdn.net/qq_26437925/article/details/145303267
    在这里插入图片描述

  • Java线程与内存
    在这里插入图片描述

  • 主内存:java虚拟机规定所有的变量(不是程序中的变量)都必须在主内存中产生;为了方便理解,可以认为是堆区。可以与前面说的物理机的主内存相比,只不过物理机的主内存是整个机器的内存,而虚拟机的主内存是虚拟机内存中的一部分。

  • 工作内存:java虚拟机中每个线程都有自己的工作内存,该内存是线程私有的;为了方便理解,可以认为是虚拟机栈。线程的工作内存保存了线程需要的变量在主内存中的副本。虚拟机规定,线程对主内存变量的修改必须在线程的工作内存中进行,不能直接读写主内存中的变量。不同的线程之间也不能相互访问对方的工作内存。如果线程之间需要传递变量的值,则必须通过主内存来作为中介进行传递。

主内存与Java工作内存之间的具体交互协议,虚拟机保证如下的每一种操作都是原子的,不可再分的(对于double,long类型的变量有例外,商用JVM基本优化了这个问题)

  • lock: 作用于主内存的变量,它把一个变量标识为一个线程独占的状态

  • unlock:作用于主内存的变量, 解锁

  • load:作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中

  • use:作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作

  • assign: 作用于工作内存的变量,它把一个从执行引擎接收到的赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作

  • store: 作用于工作内存的变量,它把工作内存中的一个变量的值传送到主内存中,以便随后的write操作使用

  • write:作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中

可见性例子和volatile

public class Main {

    private static volatile Boolean flag = true;

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

       Thread thread =  new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("A start");
                while (flag) {

                }
                System.out.println("A end");
            }
        });
        thread.start();
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (Exception e) {
        }
        flag = false;

        System.out.println("main end");

    }
}

输出如下:
在这里插入图片描述

主线程后执行,设置了flag=false,由于volatile的作用,导致线程可见flag,所以线程A可以结束。

volatile如何保证可见性

硬件层两个内存屏障:load barrier、store barrier;其有两个功能:

  • 禁止屏障前后的指令重排序
  • 强制把写缓冲区的的数据写入主内存
屏障类型指令示例说明
LoadLoad BarriersLoad1;LoadLoad;Load2该屏障确保Load1数据的装载先于Load2及其后所有装载指令的的操作
StoreStore BarriersStore1;StoreStore;Store2该屏障确保Store1立刻刷新数据到内存(使其对其他处理器可见)的操作先于Store2及其后所有存储指令的操作
LoadStore BarriersLoad1;LoadStore;Store2确保Load1的数据装载先于Store2及其后所有的存储指令刷新数据到内存的操作
StoreLoad BarriersStore1;StoreLoad;Load2该屏障确保Store1立刻刷新数据到内存的操作先于Load2及其后所有装载装载指令的操作。它会使该屏障之前的所有内存访问指令(存储指令和访问指令)完成之后,才执行该屏障之后的内存访问指令

其中StoreLoad Barriers同时具备其他三个屏障的效果,因此也称之为全能屏障(mfence),是目前大多数处理器所支持的;但是相对其它屏障,该屏障的开销相对昂贵。

volatile 正是通过加入内存屏障,禁止指令重排优化来实现可见性和有序性,即

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

所以线程写volatile变量的过程:

  1. 改变线程工作内存的中volatile变量副本的值。
  2. 将改变后的副本的值从工作内存刷新到主内存。

线程读volatile变量的过程:

  1. 从主内存中读取volatile变量的最新值到线程的工作内存中。
  2. 从工作内存中读取volatile变量的副本。

如上例子中,当主线程写flag时,会将数据刷新到主内存中;而线程thread读取的时候,也是确保读取到的是主内存数据,所有能够实现例子代码中的可见性验证。

原子性与单例模式

class Singleton{
    private byte[] data = new byte[1024];

    private static Singleton instance = null;

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

}

public class Main {


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

        for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                Singleton singleton = Singleton.getInstance();
            }).start();
        }
    }
}

多次运行,可以看到有输出如下的例子:
在这里插入图片描述

不是double check的单例模式,实际上会new出多个实例,无法实现单例模式。

因为Object o = new Object();的汇编指令如下,不是一个原子操作

0 new #2 <java/lang/Object>
3 dup
4 invokespecial #1 <java/lang/Object.<init>>
7 astore_1
8 return

一个对象创建的过程:(记住3步)

  1. 堆内存中申请了一块内存(new指令)【半初始化状态,成员变量初始化为默认值】
  2. 这块内存的构造方法执行(invokespecial指令)
  3. 栈中变量建立连接到这块内存(astore_1指令)

i++非原子性

import java.util.Random;
import java.util.concurrent.TimeUnit;

public class Main {

    public volatile static int num = 0;

    public static void add() {
        num++;
    }

    public synchronized static void addSync() {
        num++;
    }

    private final static int N = 30;

    public static void main(String[] args) throws Exception {
        Thread[] threads = new Thread[N];
        for(int i=0;i<N;i++){
            threads[i] = new Thread(()->{
                try{
                    TimeUnit.MILLISECONDS.sleep(new Random().nextInt(10));
                    int addCnt = 100;
                    for(int j=0;j<addCnt;j++){
                       add();
                    }
                }catch (Exception e){
                    e.printStackTrace();
                }
            });
            threads[i].start();
        }
        for(int i=0;i<N;i++) {
            threads[i].join();
        }
        System.out.println("num:" + num);
    }

}
/* output
小于3000的值
*/

线程安全

  • 什么是线程安全问题?

当多个线程共享同一个全局变量,做写的时候,可能会受到其它线程的干扰,导致数据有问题,这中现象叫做线程安全问题

关键词:共享数据,多线程,并发写操作

结合本文和上一篇:https://blog.csdn.net/qq_26437925/article/details/145303267 看到了原子性可见性顺序性三个重要性质,这构成了多线程线程安全编程的基础。


http://www.kler.cn/a/525937.html

相关文章:

  • 什么是Maxscript?为什么要学习Maxscript?
  • Linux——网络(tcp)
  • 【重生之我在学习C语言指针详解】
  • 【已解决】黑马点评项目Redis版本替换过程的数据迁移
  • 【每日一A】2015NOIP真题 (二分+贪心) python
  • Android createScaledBitmap与Canvas通过RectF drawBitmap生成马赛克/高斯模糊(毛玻璃)对比,Kotlin
  • 为AI聊天工具添加一个知识系统 之71 详细设计 之12 形式文法、范式语法和惯式用法
  • 2024 ICLR Spotlight Learning-Feedback
  • 网络攻防实战指北专栏讲解大纲与网络安全法
  • C语言练习(32)
  • C++,STL,【目录篇】
  • Formality:黑盒(black box)
  • 基于RAG方案构专属私有知识库(开源|高效|可定制)
  • oracle中使用in 和 not in 查询效率分析
  • 控件【QT】
  • 对于RocksDB和LSM Tree的一些理解
  • 【MySQL】数据类型与表约束
  • 设想中的计算机语言:可执行对象的构造函数和析构函数
  • Vue.js路由管理与自定义指令深度剖析
  • Python | Pytorch | Tensor知识点总结
  • 智能汽车网络安全威胁报告
  • k8s--部署k8s集群--控制平面节点
  • 春节期间,景区和酒店如何合理用工?
  • DOM操作中childNodes与children的差异及封装方案
  • 算法随笔_30: 去除重复字母
  • 显示当前绑定变量