java内存模型

一、概述
java内存模型主要目标是定义共享变量(这里的变量指的是实例字段、静态字段和数组对象)的访问规则,首先介绍一下为什么java需要定义内存模型。我们知道现在的计算机都是多核的,也就是有多个CPU,由于CPU的运行速度和内存的速度有很大数量级的差距,因此不得不在CPU和内存中间加了高速缓存,CPU取数据,首先放到高速缓存中,写完之后,再从高速缓存刷新到内存当中,当多个CPU操作同一块内存区域时,必然涉及到到缓存一致性的问题,解决缓存一致性问题的协议有很多,比如MSI、MESI、MOSI、Synapse等等,此外,为了充分利用CPU内的运算单元,处理器和操作系统可能会对代码进行乱序执行,也就是代码的执行顺序不一定会按照代码的输入顺序。不同的CPU和操作系统在这方面的处理不一样,而这一底层细节不能留给开发者自己来处理,毕竟java可是“一次编译,到处运行”,因此定义好一套内存模型,让开发人员不用管CPU和操作系统的细节,统一按照内存模型定义好的规则来处理,底层的细节由java虚拟机自己来实现,尤为重要。

二、主内存和工作内存
java内存模型定义了主内存和工作内存,主内存是所有线程共享的,工作内存是线程私有的(工作内存其实并不存在,你可以把虚拟机栈当做工作内存的一部分),所有的变量都在主内存中,当工作线程要对主内存的数据进行访问的时候,首先需要拷贝到工作内存中,在工作内存操作完成之后,再刷新到主内存中,和上面CPU类似,也会存在变量的可见性和一致性问题。


下图是java内存模型和硬件内存架构映射图

三、java内存模型的基本原则
1.原子性
原子性变量的操作,包括read、load、assign、use、store、write,可以理解为对基本变量的访问读写是具有原子性的,比如:
x=1
但是下面就不具有原子性啦
y=x
因为涉及到将x加载进来,再赋值给y
注:32位的虚拟机中,long和double的赋值操作并不是原子性的,这两种类型是64位的,涉及到2次32位的操作,而64位的虚拟机中,是原子操作
2.有序性(as-if-serial)
单线程情况下,指令重排时,对于有数据依赖的情况下啊,不允许重排,换句话理解,就是不管怎么重排,单线程情况下的执行结果不能被改变,这个是从编译器到处理器都必须遵循的原则,之所以强调单线程,是因为,多线程情况下,一个线程对于另一个线程来说是无序的。
比如下面的操作,就不允许指令重排序
x=1
y=x
3.先行原则(happens-before)
一个操作对于另一个操作可见,必然存在happens-before,这两个操作可以是在同一个线程中,也可以是在不同的线程当中,这里的happens-before原则,并不是指操作的执行顺序,仅仅是指执行结果的可见性,下面举个例子:
线程A执行:
x=1
线程B执行:
y=x
先行原则规定,如果要求线程A的x=1操作happens-before线程B,那么y=x操作中,y肯定等于1,如果没有要求先行原则,那么y=x的结果就是不定的。另外举一个同一个线程的操作。
线程A:
x=1
y=2
x=1操作先行于y=2操作,但是不代表x=1在时间上比y=2先执行,由于不存在依赖,存在指令重排问题,因此y=2可能比x=1先执行,但这并没有影响先行原则的结果正确性,因此也是可以接受的。
四、volatile关键字规则
1.内存屏障
内存屏障是CPU指令中就支持的,java虚拟机在实现的时候,使用了汇编来调用,内存屏障保证了共享变量的一致性问题,内存屏障的作用,主要是把缓存中的数据刷新到主内存中,在java内存模型中,你可以认为是把工作内存的数据刷新到主内存中,具体的细节可以在网上搜索
2.volatile实现同步功能
用volatile关键字修饰的变量,java虚拟机在实现的时候,也是插入内存屏障来实现变量的可见性的,当对一个volatile关键字进行读之前,会在指令之前插入内存屏障,这样变量的值就会刷到主内存中,我们读到的volatile关键字的值也就是最新值了,在对一个volatile关键字做写操作,会在操作完成的后面添加内存屏障,这样写完之后,变量也会刷新到主内存中。因此就实现了一个线程对于volatile变量的读写操作,对于其他线程都是可见的啦。
五、synchronized关键字
volatile实现的是轻量级的同步,synchronized则实现粒度比较大的同步机制,synchronized会阻止其他线程获取当前对象的监控锁,synchronized也是使用的内存屏障,所不同的是,它会把它所包含的代码块全部刷新到主内存中,volatile关键字修饰的变量,这个变量的读写都是可见的,但是synchronized包含的代码块,只是本次的,如果要保证一个对象的读写方法让变量保持同步,则需要加两个同步。如下:
public class A{
    private int value;

    public void setValue(int value){
        this.value = value;
    }

    public int getValue() {
        return value;
    }
}
那么我们想要保证value的同步,需要在setValue和getValue操作中同时加synchronzied关键字,这样保证了,每次操作完之后,变量的读写可以从主内存中获取到最新的数据,或者马上同步到主内存中,被其他线程可见,这里仅仅是2个方法的调用,如果不在两个方法的范围内,就不一定了。
六、其他
synchronized用的是锁机制,因为性能的原因,java里面有很多锁优化的策略,这里只是做大概介绍,具体细节网上搜索。
1.自旋锁
因为线程的挂起和唤醒涉及到CPU中上下文的切换,也就是CPU的内核态和用户态的切换,代价比较大,因此java中会采用自旋锁,来稍微优化,也就是线程在很小的时间内做空操作,尝试获取锁,如果获取到就继续执行,没有获取到并超过了时间,则再挂起,不过这个有一个问题是做空操作的会浪费cpu资源,然后有了自适应自旋锁。
2.锁消除
如果能够判断某段代码永远不会存在资源竞争问题,就把锁给去掉
3.锁粗化
原则上锁的粒度越小越好,但是当一些情况下,比如对同一个对象频繁加锁和解锁,或者干脆在循环体里对对象加锁,因此可以把锁的粒度变大,避免这种极端情况
4.轻量级锁
虚拟机的对象的对象头部,有一个Mark Word用于存储对象运行时的数据,有一个标志位保存了锁的状态,线程需要访问这一对象时,会通过CAS(compare and set,也需要CPU支持的原子操作)操作尝试改变这一标志,如果成功,则进入同步块中执行,如果失败,首先检查是否是被当前线程锁定了的,如果是,则继续进入同步块中执行,如果还不是,那么就等待,如果同一个变量是2个以上的线程竞争,则标志位为重量锁,后面等待的锁进入阻塞
5.偏向锁
上面的轻量锁我们看到同一个线程访问一个变量两次,会有2个CAS操作,于是偏向锁继续做了优化,如果是第一次通过CAS操作获取到了锁,那么下一次操作还是这个线程的话,直接进入代码块中执行,不需要再进行同步操作,如果下一次不是这个线程了,那么偏向锁也宣告失败,被另一个线程占用,进入和刚才同样的循环。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值