Java内存模型

1. 主内存与工作内存

主内存:指的就是操作系统内存
工作内存:指的是创建线程所使用的内存

线程对变量的读写操作不是直接对主内存操作的,而是在工作内存操作的。

线程、主内存、工作内存三者的交互关系就和下图一样:
在这里插入图片描述

2. 内存间交互操作

Java内存模型中定义了8种操作来完成一个变量从主存中拷贝到工作内存、从工作内存同步回主存的实现细节。JVM实现时必须保证以下这8种操作的每一种操作都是原子的、不可再分的。

  • lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态
  • unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
  • read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
  • use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量
  • store(存储):作用于工作内存的变量,它把工作内存中的一个变量值传送到主内存中,以便后续的write操作使用
  • write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中

Java内存模型的三大特性

  • 原子性:由Java内存模型来直接保证的原子性变量操作包括read、load、use、assign、lock、unlock、store、write。8大基本数据类型的访问读写也是具备原子性的。
  • 可见性:可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。volatile、synchronized、final三个关键字可以实现可见性。
  • 有序性:如果在本线程内观察,所有的操作都是有序的;如果在线程中观察另外一个线程,所有的操作都是无序的(例:A线程观察它自己的操作是有序的,但B线程观察A线程的程序是无序的)。之所以会无序是因为“指令重排”和“工作内存与主内存同步延迟”现象。
    Java内存模型具备一些先天的“有序性”,也就是说不需要任何手段就能够得到保证的有序性,这个也称为happens-before原则。

happens-before原则(先行发生原则):

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  • 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的

final可以实现可见性是因为final修饰的变量不可以再被修改了,意味着每个线程去看都是它最开始初始化的那个值。

synchronized可以实现可见性是因为它修饰同步代码块,一个线程完了之后其他线程可以访问。

volatile可以实现可见性是因为volatile型变量的特殊规则

3. volatile型变量的特殊规则

当一个变量被volatile修饰后,它具备以下两种特性:

一、保证此变量对所有线程的可见性
volatile变量在各个线程中是一致的,但是volatile变量的运算在并发下一样是不安全的
例:

public class Test {
    public static volatile int num = 0;
    public static void increase() {
        num++;
    }
    public static void main(String[] args) {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 100; j++) {
                        increase();
                    }
                }
            });
            threads[i].start();
        }
        System.out.println(num);
    }
}

它的运行结果每次都是不一样的:
在这里插入图片描述
在这里插入图片描述
为什么会出现这样的问题呢?
是因为num++等同于num = num + 1,不满足原子性。volatile关键字保证了num的值在取值时是正确的,但是在执行num+1的时候,其他线程可能已经把num值增大了,这样在+1后会把较大的数值同步回主内存之中。

volatile关键字只保证可见性,在不符合以下两条规则的运算场景中,仍需要通过加锁(synchronized或者lock)来保证原子性。

  1. 运算结果并不依赖变量的当前值,或者不能够确保只有单一的线程修改变量的值
  2. 变量需要与其他的状态变量共同参与不变约束

二、使用volatile变量的语义是禁止指令重排序。普通变量只会保证执行的结果正确,不能保证变量赋值操作的顺序和代码中执行的顺序一致,也就是说字节码的执行顺序和自己写的代码顺序不一定一致。

禁止指令重排的意思是:

1.volatile修饰的变量的前后代码的执行顺序是不能颠倒的
在这里插入图片描述
2. 在进行指令优化时,不能将在对volatile变量访问的语句放在其后面执行,也不能把volatile变量后面的语句放到其前面执行。

例:

//x、y为非volatile变量
//flag为volatile变量
x = 2; //语句1
y = 0; //语句2
flag = true; //语句3
x = 4; //语句4
y = -1; //语句5

假设没有语句3,则在优化的时候,就没有语句1和2了,但是如果有语句3的话,执行到语句3的时候,语句1和2必定是执行完毕的,并且语句1和2的执行结果对语句3、4、5都是可见的。

指令重排一般会发生的问题是,如果把volatile修饰的变量放的位置不对,程序可能会间歇性出现bug,今天好了,明天又出错了,所以出现这种问题的时候,首先考虑是不是volatile修饰的变量放的位置不对。
假如在B线程里需要用到A线程的结果,那就得把volatile修饰的变量放在A线程代码和B线程代码中间,必须保证A先执行完,如果volatile修饰的变量位置放的不对,那就可能在指令重排的时候先指令B的代码,这时A还没执行,所以就会出错。

单例模式中的双重检验锁模式
双重检验是指两次检查 instance == null,一次是在同步块外,一次是在同步块内。
为什么要检查两次呢?
因为有可能多个线程一起进入同步块外的if,如果不在同步块内进行二次检验的话就会生成多个实例。

public static Singleton getInstance(){
        if(instance == null){//第一次检测
            synchronized (Singleton.class){
                if(instance == null){//第二次检测
                    instance = new Singleton();
                }
            }
        }

上边这段代码还是有问题的,因为instance = new Singleton()不是一个原子操作。JVM实际上为这句话做了三件事:

  1. 给 instance 分配内存
  2. 调用 Singleton 的构造函数来初始化成员变量
  3. 将instance对象指向分配的内存空间,执行完这步,instance就是非null了

但是JVM即时编译器会发生指令重排的优化,也就是说上边的顺序可能是1-2-3,也可能是1-3-2。
如果执行顺序的1-3-2,则在第3步执行完毕,第2步执行之前,被线程二抢占了,这时instance已经是非null了(但是并没有初始化),所以线程二会返回instance,然后使用,这就会报错。
要解决这个问题也很简单,我们只需要把instance用volatile关键字修饰,禁止指令重排。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值