Java内存模型

Java内存模型

Java虚拟机规范定义了一种Java内存模型,屏蔽了底层硬件和操作系统之间的差异,从而支撑了Java所声称的跨平台编程。

硬件内存模型

为了理解Java内存模型,需要先理解物理内存模型,如下图所示。计算机存储系统是由寄存器、高速缓存、物理内存构成的(这里不介绍硬盘存储)。其中寄存器是在CPU中的,存储大小最小,速度最快。高速缓存介于寄存器和物理内存之间,存储大小介于寄存器和物理内存。物理内存存储最大,速度最慢。
CPU读取数据时,会先从主存中读取一部分数据到缓存中,进而读取缓存数据到寄存器。CPU写数据时会先写数据到缓存中,然后再把缓存中的数据写到主存中。
这里写图片描述

主内存和工作内存

Java内存模型规定了主内存工作内存的概念,主内存存储了所有的变量,可以类比到物理硬件的内存。工作内存是每个线程私有的,可以类比到物理硬件的高速缓存,工作内存中的变量是主内存的拷贝。线程对变量的所有操作都必须基于工作内存进行。线程间变量值的的传递是通过主内存进行的。

Java内存模型操作

  • lock(锁定):作用于主内存的变量,它把一条变量标记为本线程所独占的。

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

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

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

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

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

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

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

    如果需要把一个变量从主内存复制到工作内存中,按顺序操作read、load,不需要连续操作

    如果需要把一个变量从工作内存复制到主内存中,按顺序操作store、write,不需要连续操作

Java内存模型操作需要满足的规则

  • read和load、store和write不允许单独出现,即不允许从主内存读取了值,但是工作内存不接收。不允许向主内存回写值,但是主内存不接收的情况。
  • 不允许一个线程丢弃它最近的assign操作,即一个线程改变了一个变量值,但是没有同步回主内存。
  • 不允许一个线程无原因的(没有发生过assign操作)把数据从工作内存同步回主内存。
  • 一个新的变量只能在主内存中”诞生“,不允许在工作内存中直接使用一个未被初始化的变量。
  • 一个变量在同一时间只能允许一条线程对其进行lock操作,但lock操作可以被同一条线程执行多次。多次执行时,只有执行相同次数的unlock操作,变量才会被解锁。
  • 对一个变量执行lock操作前,将会清空此变量在工作内存中的值,在执行引擎使用这个变量的时候,需要重新执行load或assign操作初始化该变量的值。
  • 如果一个变量没有被lock,那就不能对它执行unlock操作,一个线程也不能unlock被其他线程lock的锁。
  • 对一个变量执行unlock前,必须把此变量同步回主内存中(执行store、write操作)。

volatile变量

volatile变量是java提供的最轻量级的同步机制。当一个变量被定义为volatile时意味着该变量具有两个特性,第一是保证此变量对所有线程的可见性,第二是禁止指令重排序优化。

变量可见性

当一个线程修改了某个变量的值,该值对于其他线程是立即可见的,普通的变量不具备这个特性。

那么volatile变量如何保证可见性的?

  • 对于线程读来说,每次都是从主内存读取数据,能够保证读到最新的值。
  • 对于线程写来说,每次写完后会立即将该值刷新回主内存。操作序列为:assign(赋值)、store(存储)、write(写入),连续执行。

注意:即使volatile变量能够保证可见性,但是在并发下也不是线程安全的,例如对于如下代码

public class Test {

  private static volatile Integer count = 0;
    private static final Integer THREADS_COUNT = 20;
    private static CountDownLatch countDownLatch = new CountDownLatch(THREADS_COUNT);

    private static void increase() {
        count++;
    }

    public static void main(String[] args) {
        Thread[] threads = new Thread[THREADS_COUNT];
        for (int i = 0; i < THREADS_COUNT; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 10000; j++) {
                        increase();
                    }
                    countDownLatch.countDown();
                }
            });
            threads[i].start();
        }
        while (countDownLatch.getCount() != 0) {
        }
        System.out.println(count);
    }
}

运行之后输出结果基本上都小于200000,原因在于”count++”操作不是原子操作,当线程A还在执行递增操作时,线程B可能已经将该值加大了,那么线程B所做的自增操作其实是没有效果的。

指令重排序

普通的变量只能保证在该方法的执行过程中最终依赖赋值结果的地方都能获得正确的结果,而不能保证变量赋值的顺序和程序代码的顺序一致,CPU可能会对指令进行重排序,在该方法中的线程不会感觉到这个重排序操作。这就是Java内存模型中描述的”线程内表现为串行的语义”。volatile变量可以避免指令重排序的发生。

指令重排序的原理是在变量赋值操作后加了一个内存屏障,使得内存屏障后面的指令不能重排序到内存屏障前面的位置。指令屏障的原理(lock操作)是将本CPU核的cache写入内存(store、write操作),同时也会引起别的核的cache无效化,从而保证了对变量的修改对其他核的立即可见性。

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

  • read–>load–>use操作必须连续一起出现。这条规则要求每次使用一个volatile变量之前,都必须从主内存中刷新出最新的值,可以保证其他线程对该变量的修改对本线程是可见的。
  • assign–>store–>write操作必须连续一起出现。这条规则要求每次对一个volatile变量赋值之后,都必须将其写回主内存中,保证本线程对该变量的修改对其他线程是立即可见的。
  • volatile修饰的变量不会被指令重排序,保证代码的执行顺序与程序的顺序相同。

long和double变量的特殊规则

java内存模型要求lock、unlock、read、load、use、assign、store、write这8种操作具有原子性,但是对long和double这种64位的操作,java内存模型没有要求这8种操作必须具有原子性,但是建议虚拟机实现为原子操作,因此基本上所有的虚拟机平台都已经将long和double实现为原子操作。

普通变量和volatile变量的区别

volatile变量可以保证新值能够立即同步到主内存,以及每次使用时都会从主内存刷新最新的值。而普通变量不能保证这点。

同步块(synchronized)

jvm提供了monitorenter和monitorexit字节码指令隐式的使用了lock和unlock操作。对一个变量执行unlock操作前必须把该变量同步回主内存中(store、write)

final变量(不可变变量)

被final修饰的变量一旦在构造器中初始化完成,并且this引用没有传递出去(如果this引用逃逸出去,其他线程可能看到初始化一半的对象),那其他线程就能看到final字段的值。

有序性

如果在本线程内观察,所有的操作都是有序的(线程内表现为串行的语义)。如果在一个线程中观察另一个线程,所有的操作都是无序的(指令重排序现象、工作内存与主内存同步延迟现象)。

先行发生原则

如果操作A先行发生于操作B,其实就是说在发生操作B之前,操作A的影响能被操作B观察到。

虚拟机与生具来的先行发生关系包括如下几条:

  • 程序次序规则:在一个线程内,程序代码的前面部分先行发生于代码的后面部分。
  • 管程锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。
  • volatile变量规则:对一个volatile变量的写操作先行发生于后面对该变量的读操作。
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。
  • 线程终止规则:线程中的所有操作都先行发生于对此线程的终止操作。
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
  • 对象终结规则:一个对象的初始化完成先行发生于该对象的finalize()方法的开始。
  • 传递性:如果操作A先行发生于操作B,操作B先行发生于操作C,那么操作A先行发生于操作C。

时间先后顺序与先行发生原则之间没有太大的关系,一切要以先行发生原则为准则。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值