5分钟系列之-java内存模型与线程(二、java内存模型)

java内存模型

        java虚拟机定义一种规范来屏蔽掉各种硬件和操作系统的内存访问差异,使Java程序在各种平台下都能达到一致的内存访问效果。

1.主内存与工作内存

        java内存模型定义程序中各变量的访问规则。这里的变量是共享的变量,不包括局部变量和方法参数。java内存规定了所有变量都存储在主内存中,每条线程有自己的工作内存,线程对变量的所有操作(读取赋值等)都必须在自己的工作内存中进行,不能直接读写主内存中的变量;不同的线程间也无法直接访问对方的工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。如下图所示

在这里插入图片描述

        从更底层次上说,主内存就直接对应物理硬件的内存,为了获取更好的运行速度,虚拟机可能会让工作内存优先存储在寄存器和高速缓存中。

2.内存间的交互操作

        虚拟机实现时必须保证下边的每一种操作都是原子的,不可再分的。

2.1八种操作

        lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。

        unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。

        read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。

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

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

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

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

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

以上8项看着复杂,仔细读一遍其实也没那么复杂,其中绿色四个是作用在主内存,红色四对作用在工作内存,实际上就是一个变量要进行运算所需经过的几个过程。

2.2八种操作需要满足的规则

        1.【顺序】要把一个变量从主内存复制到工作内存,那就要顺序地执行read和load操作,如果要把变量从工作内存同步回主内存,就要顺序地执行store和 write操作。(说的时顺序不是连续)

        2.【成对】不允许read 和load、store 和 write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况出现。

        3.【丢弃】不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存

        4.【不做】不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存中

        5.【出生地】一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说,就是对一个变量实施use、store操作之前,必须先执行过了assign 和load操作

        6.【单锁】一个变量在同一个时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。

        7.【清空】如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值。

        8.【乱解】如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定住的变量。

        9.【同步】对一个变量执行 unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)

这8中内存访问操作以及上述规定再加上volatile的一些特殊规定完全确定了java程序中哪些内存访问操作再并发下时安全的

ps:抽字串联记忆方法-程序(员对)丹青不(了)解不(要)生气
程【成对】序【顺序】(员对)丹【单锁】青【清空】不【同步】解【乱解】不【不做】(要)生【出生地】气【丢弃】

2.3 对于volatile变量的特殊规则
2.3.1.可见性:

        当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。
volatile变量在各个线程的工作内存中不存在一致性问题,但是java的运算并非原子操作,导致volatile变量在并发运算的情况下一样是不安全的。

        拿i++操作举例,当多个线程对i进行++操作时,每个线程的工作内存只能保证使用之前会从主内存拿到最新的值,但是在执行加的操作时可能其他线程已经把值修改完并同步回主存,这时在自己工作内存中的数据已经是过期,所以在执行完+的操作后就把较小的值同步回主存了。

        由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍通过加锁(使用 synchronized 或 java.util.concurrent 中的原子类)来保证原子性。

        1.运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。

        2.变量不需要与其他的状态变量共同参与不变约束。

2.3.2.禁止指令重排序

        昨天在上文中已经举例什么是指令重排序,今天不妨看另外一个例子

        Map configOptions;
        char[] configText;
        //1此变量必须定义为volatile
        volatile boolean initialized = false;
        //假设以下代码在线程A中执行
        //模拟读取配置信息,当读取完成后将initialized设置为true以通知其他线程配置可
        configoptions = new HashMap();
        configText = readConfigFile(fileName);
        processConfigOptions(configText, configOptions);
        initialized = true;
        //假设以下代码在线程B中执行
        //等待initialized为true,代表线程A已经把配置信息初始化完成
        while (!initialized) {
            sleep();
        }
        //使用线程A中初始化好的配置信息
        doSomethingWithConfig();

        如果定义变量initialized变量时没有使用volatile修饰,就可能由于指令重排序的优化,导致位于线程A中最后一句的代码“initialized=true” 被提前执行,这样在线程b中使用配置信息就可能出错;
    volatile禁止使用指令重排序用到了内存屏障,重排序时不能把后边的指令重排序到内存屏障前的位置。

2.3.3 Java内存模型对volatile变量的特殊规定

        1.在工作内存中每次使用volatile变量前都要从主内存刷新最新的值。

        2.在工作内存中每次修改volatile变量后必须立刻同步回主存,保证对其他线程可见

        3.保证volatil变量不会被指令重排序优化,保证代码顺序与程序顺序相同。

2.4 对于long和double型变量的特殊规则

        对于64的数据类型,在模型中特别定义了一条相对宽松的规定:允许虚拟机将没有被volatile修饰的变量差分为两次32位的操作,即理论上可能出现读取到半个变量的情况,不过目前商用虚拟机中不会出现。

2.5原子性、可见性与有序性

        来看以下哪些操作实现了这三个特性
原子性:
        1.能保证基本类型操作访问时具备原子性的

        2.更大范围的原子性可以使用lock,unlock实现,也可以使用字节码指令monitorenter和monitorexit来隐式实现,即synchronized关键字

可见性:
        java内存模型是通过将修改过的值同步回主存实现的,其中volatile变量能改完立即同步回主存,使用前立即从主存刷新。除了volatile之外还有两个关键字能保证可见性,synchronized,和final

有序性
        java提供了两个关键字来保证有序性,volatile和synchronized

2.6 先行发生原则

        如果说一个操作A先行发生与B,那么就是说发生操作B前,操作A产生的影响都能被B观察到。

        java中有一些天然的先行发生关系如下:

        1.【程序】程序次序规则(Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。

        2.【管程】管程锁定规则(Monitor Lock Rule):一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而“后面”是指时间上的先后顺序。

        3.【变量】volatile 变量规则(Volatile Variable Rule):对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后顺序。

        4.【线程】线程启动规则(Thread Start Rule):Thread 对象的start()方法先行发生于此线程的每一个动作。

        5.【线程】线程终止规则(Thread Termination Rule):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。

        6.【线程】线程中断规则(Thread Interruption Rule):对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 Thread.interrupted()方法检测到是否有中断发生。

        7.【对象】对象终结规则(Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。

        8.【传递】传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。

ps:抽字串联记忆法-城管变现递对象
城【程序】管【管程】变【变量】现【线程】递【传递】对象【对象】


欢迎关注我的公众号,加我好友【chenaima】,一起探讨,共同进步!

​​​​ 在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值