首页 > 系统相关 >一篇文章告诉你什么是Java内存模型

一篇文章告诉你什么是Java内存模型

时间:2023-05-23 09:11:21浏览次数:48  
标签:Java 一篇 int flag 线程 内存 volatile

在上篇 并发编程Bug起源:可见性、有序性和原子性问题,介绍了操作系统为了提示运行速度,做了各种优化,同时也带来数据的并发问题,

定义

在单线程系统中,代码按照顺序从上往下顺序执行,执行不会出现问题。比如一下代码:

int a = 1;
int b = 2;
int c = a + b;

程序从上往下执行,最终c的结果一定会是3

但是在多线程环境中,代码就不一定会顺序执行了。代码的运行结果也有不确定性。在开发中,自己本地没问题,一行行查看代码也没有问题,但是在高并发的生产环境就会出现违背常理的问题。

多线程系统提升性能有如下几个优化:

  • 单核的cpu改成多核的cpu,每个cpu都有自己的缓存。
  • 多个线程可以在cpu线程切换。
  • 代码可能根据编译优化,更新代码的位置。

这些优化会导致可见性原子性以及有序性问题,为了解决上述问题,Java内存模型应运而生。

Java内存模型是定义了Java程序在多线程环境中,访问共享内存和内存同步的规范,规定了线程之间的交互方式,以及线程与主内存、工作内存的的数据交换。

Java内存模型解决并发

导致可见性的原因的是缓存,导致有序性的问题是编译优化,那解决可见性、有序性问题就是禁用缓存和编译优化。这样虽然解决了并发问题,但是性能却下降了。

合理的方案就是按需求禁用缓存和编译优化,在需要的地方添加对应的编码即可。Java内存模型规范了JVM如何按需禁用缓存和编译优化,具体包括volatilesynchronizedfinal这几个关键字,以及Happens-Before规则。

可见性问题

在多核cpu操作系统中每次cpu都有自己的缓存,cpu先从内存获取数据,再进行运算。比如下图中线程A和线程B,分别运行自己的cpu,然后从内存获取变量到自己的cpu缓存中,并进行计算。

线程B改变了变量之后,线程A是无法获取到最新的值。以下代码中,启动两个线程,线程启动完线程A,循环获取变量,如果是true,一直执行循环,直到被改成false才跳出循环,然后再延迟1s启动线程B,线程修改变量值为true:

private static boolean flag = true;

// 线程A一直读取变量flag,直到变量为false,才跳出循环
class ThreadA extends Thread {
    @Override
    public void run() {
        while (flag) {
            // flag 为 true,一直读取flag字段,flag 为 false 时跳出来。
            //System.out.println("一直在读------" + flag);
        }
        System.out.println("thread - 1 跳出来了");
    }
}
// 1s 后线程B将变量改成 false
class ThreadB extends Thread {

    @Override
    public void run() {
        System.out.println("thread-2 run");
        flag = false;
        System.out.println("flag 改成 false");
    }
}

@Test
public void test2() throws InterruptedException {
    new Thread1().start();
    // 暂停一秒,保证线程1 启动并运行
    Thread.sleep(1000);
    new Thread2().start();
}

运行结果:

thread-2 run
flag 改成 false

线程A一直处于运行中,说明线程B修改后的变量,线程A并未知道。

flag变量添加volatile声明,修改成:

private static volatile boolean  flag = true;

再运行程序,运行结果:

thread-2 run
flag 改成 false
thread - 1 跳出来了

线程B运行完后,线程A也跳出了循环。说明修改了变量后,其他线程也能获取最新的值。

一个未声明volatile的变量,都是从各自的cpu缓存获取数据,线程更新数据之后,其他线程无法获取最新的值。而使用volatile声明的变量,表明禁用缓存,更新数据直接更新到内存中,每次获取数据都是直接内存获取最新的数据。线程之间的数据都是相互可见的。

可见性来自happens-before规则,happens-before用来描述两个操作的内存可见性,如操作Ahappens-before操作B,那么A的结果对于B是可见的,前面的一个操作结果对后续操作是可见的happens-before定义了以下几个规则:

  • 解锁操作happens-before同一把锁的加锁操作。
  • volatile 字段的写操作happens-before同一字段的读操作。
  • 线程的启动操作happens-before该线程的第一个操作。
  • Ahappens-beforeB,且Bhappens-beforeC,那么Ahappens-beforeC。happens-before具有传递性。

有序性问题

先看一个反常识的例子:

int a=0, b=0;
public void method1() {
    b = 1;
    int r2 = a; 
}

public void method2() {
    a = 2; 
    int r1 = b; 
}

定义了两个共享变量ab,以及两个方法。第一个方法将共享变量b赋值为1 ,然后将局部变量r2赋值为a。第二个方法将共享变量a赋值为2,然后将局部变量r1赋值为b

在单线程环境下,我们可以先调用第一个方法method1,再调用method2方法,最终得到r1r2的值分别为1,0。也可以先调用method2,最后得到r1r2的值分别为0,2

如果代码没有依赖关系,JVM编译优化可以对他们随意的重排序,比如method1方法没有依赖关系,进行重排序:

int a=0, b=0;
public void method1() {
    int r2 = a; 
    b = 1;
}

public void method2() { 
    int r1 = b; 
    a = 2;
}

此时在多线程环境下,两个线程交替运行method1method2方法:

重排序后r1r2分别是0,0

那如何解决重排序的问题呢?答案就是将变量声明为volatile,比如a或者b变量声明volatile。比如b声明为volatile,此时b的赋值操作要happens-before r1的赋值操作。

int a=0;
volatile int b=0;
public void method1() {
    int r2 = a; 
    b = 1;
}

public void method2() { 
    int r1 = b; 
    a = 2;
}

同一个线程顺序也满足happens-before关系以及传递性,可以得到r2的赋值happens-before a的赋值。也就表明对a赋值时,r2已经完成赋值了。也就不可能出现r1r200的结果。

内存模型的底层实现

Java内存模型是通过内存屏障来实现禁用缓存和和禁用重排序

内存屏障会禁用缓存,在内存写操作时,强制刷新写缓存,将数据同步到内存中,数据的读取直从内存中读取。

内存屏障会限制重排序操作,当一个变量声明volatile,它就插入了一个内存屏障,volatile字段之前的代码只能在之前进行重排序,它之后的代码只能在之后进行重排序。

总结

Java内存模型(Java Memory Model,JMM)定义了Java程序中多线程之间共享变量的访问规则,以及线程之间的交互行为。它规定了线程如何与主内存和工作内存交互,以确保多线程程序的可见性、有序性和一致性。

  • 可见性:使用volatile声明变量,数据读取直接从内存中读取,更新也是强制刷新缓存,并同步到主内存中。

  • 有序性:使用volatile声明变量,确保编译优化不会重排序该字段。

  • Happens-Before: 前面一个操作的结果对后续操作是可见的

参考

标签:Java,一篇,int,flag,线程,内存,volatile
From: https://www.cnblogs.com/jeremylai7/p/17422307.html

相关文章

  • ARM64启动汇编和内存初始化(上)
    文章代码分析基于linux-5.19.13,架构基于aarch64(ARM64)。涉及页表代码分析部分:(1)假设页表映射层级是4,即配置CONFIG_ARM64_PGTABLE_LEVELS=4;(2)虚拟地址宽度是48,即配置CONFIG_ARM64_VA_BITS=48;(3)物理地址宽度是48,即配置CONFIG_ARM64_PA_BITS=48;1.入口分析1.1链接脚本arch/a......
  • java学习日记20230522-TreeSet
    有序键值对集合publicclassTreeSetExercise{publicstaticvoidmain(String[]args){Integerinteger=newInteger(10);TreeSettreeSet=newTreeSet(newComparator(){@Overridepublicintcompare(Objecto1,Obj......
  • java学习日记20230522-集合选择原则
    1.判断存储的类型,一组对象【单列】或者一组键值对【双列】2.一组对象【单列】:collection的子类:允许重复:List的某个实现类:增删多LinkedList(底层维护的是双向链表)                                改查多ArrayList(底层维护的是object类型的可......
  • JavaScript函数
    1函数定义使用function关键字来定义,即functionfName(para,...){statment;...;},可使用在函数声明语句与函数定义表达式这两种形式中函数名称标识符fName。是函数声明语句必需的部分。它的用途就像变量的名字,新定义的函数对象会赋值给这个变量但对函数定义表达式来说......
  • Java设计模式-组合模式
    简介在软件设计中,设计模式是一种被广泛接受和应用的经验总结,旨在解决常见问题并提供可复用的解决方案。组合模式是一种结构型设计模式,它允许将对象组合成树形结构以表示“部分-整体”的层次结构。这种模式能够使客户端以一致的方式处理单个对象和对象集合,将对象的组合与对象的使......
  • Java-Servlet解析
    前言从事Javaweb项目开发有一段时间了,一直不理解它是怎么一回事,后来查询资料发现这里面涉及到几个东西,分别是tomcat、JavaEE中13个规范之一的servlet、以及springMVC。于是就去学习了一下,发现这里里面都是围绕这servlet进行的操作。于是就有了今天的这个总结。Servlet定义Servl......
  • 基于JAVA的springboot+vue“智慧食堂”设计与实现,食堂管理系统,附源码+数据库+lw文档+P
    1、项目介绍本系统的用户可分为用户模块和管理员模块两大界面组成。一个界面用于管理员登录,管理员可以管理系统内所有功能,主要有首页,个人中心,用户管理,菜品分类管理,菜品信息管理,留言板管理,系统管理,订单管理等功能;另一界面用于用户登录,用户进入系统可以实现首页,菜品信息,留言板,个人......
  • QSharedPointer创建导致内存泄漏,以及析构异常的问题
    1,下面的代码导致了内存泄漏autoitem=QSharedPointer(newMyClass(),&QObject::deleteLater)2,去掉了自定义析构后,内存泄漏问题解除,但是导致了新的问题,autoitem=QSharedPointer<MyClass>(newMyClass())在item析构时出现了析构错误:QCoreApplication::sendEvent:“cann......
  • idea中jdk11用maven编译失败 Fatal error compiling: tools.jar not found: XX\Java
    ideamaven编译需要用到jdk的lib包里面的tools.jar文件,但是jdk1.8之后就没有tools.jar了。我这里用的是graalvm的jdk11,编译一直报错,网上也查不到。解决办法: 根据对应路径创建一个lib包,并把jdk1.8的lib下面的tools.jar复制一个放到这个包下面,让这个路径有这个包就行了。我......
  • java基于joda-date实现获取两个时间段对应类型的所有时间,比如说两年之间的所有日期,两
    /***获取两个时间段对应类型的所有时间**@paramtype日期类型,包含day、month、year*@parambeginTime开始时间*@paramendTime结束时间*@return*/publicstaticList<String>getBetweenTime(Stringtype,String......