并发一:Java内存模型和Volatile

并发一:Java内存模型和Volatile

1、Java内存模型(JMM)

Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和在内存中取出变量的底层细节,是围绕着在并发过程当中如何处理原子性,可见性和有序性这3个特性创建的java

JMM规则

  1. 变量包含实例字段,静态字段,构成数组对象的元素,不包含局部变量和方法参数。
  2. 变量都存储在主内存
  3. 每一个线程都有本身的工做内存,工做内存保存了被该线程使用到的变量的主内存副本拷贝
  4. 线程对变量的全部操做都只能在工做内存,不能直接读写主内存的变量
  5. 不一样线程之间没法之间访问对方工做内存中的变量

主内存,线程,工做内存关系图

定义一个静态变量:static int a = 1;
线程A工做内存 | 指向 | 主内存 | 操做
-- | -- | -- | --
-- | -- | a = 1 | --
a = 1 | <-- | a = 1 | 线程A拷贝主内存变量副本
a = 3 | -- | a = 1 | 线程A修改工做内存变量值
a = 3 | --> | a = 3 | 线程A工做内存变量存储到主内存变量数组

上面的一系列内存操做,在JMM中定义了8种操做来完成安全

JMM交互

主内存和工做内存之间的交互,JMM定义了8种操做来完成,每一个操做都是原子性的多线程

  1. lock(锁定):做用于主内存变量,把一个变量标识为一条内存独占的状态
  2. unlock(解锁):做用于主内存变量,把lock状态的变量释放出来,释放出来后才能被其余线程锁定
  3. read(读取):做用于主内存变量,把一个变量的值从主内存传输到工做内存中
  4. load(载入):做用于工做内存变量,把read操做的变量放入到工做内存副本中
  5. use(使用):做用于工做内存变量,把工做内存中的变量的值传递给执行引擎,每当虚拟机遇到须要这个变量的值的字节码指令时都执行这个操做
  6. assgin(赋值):做用于工做内存变量,把从执行引擎收到的值赋值给工做内存变量,每当虚拟机遇到须要赋值变量的值的字节码指令时都执行这个操做
  7. store(存储):做用于工做内存变量,把工做内存中的一个变量值,传送到主内存
  8. write(写入):做用于主内存变量,把store操做的从工做内存取到的变量写入主内存变量中

2、volatile

当引入线程B的时候
定义一个静态变量:static int a = 1;
操做顺序 | 线程A工做内存 | 线程B工做内存 | 指向 | 主内存 | 操做
-- | -- | -- | -- | -- | --
-- | -- | -- | -- | a = 1 | --
1 | a = 1 | -- | <-- | a = 1 | 线程A拷贝主内存变量副本
2 | a = 3 | -- | -- | a = 1 | 线程A修改工做内存变量值
3 | a = 3 | -- | --> | a = 1 | 线程A工做内存变量存储到主内存变量,主内存变量还未更新
4.1 | a = 3 | a = 1 | <-- | a = 3 |线程B拷贝主内存变量副本随后主内存变量更新线程A工做内存变量
4.2 | a = 3 | a = 1 | <-- | a = 3 |线程A工做内存变量存储到主内存变量随后线程B获取主内存变量副本
操做4的时候可能出现:1.线程A变量值还未保存到主内存变量,2.线程A变量值保存到主内存变量。使用volatile关键字解决这个问题并发

public static volatile int a = 1;

特性

  1. 保证此变量对全部线程可见,一条线程修改的值,其余线程对新值能够当即得知
  2. 禁止指令重排序

可见性

修改内存变量后马上同步到主内存中,其余的线程马上得知得益于Java的先行发生原则app

先行发生原则中的volatile原则:一个volatile变量的写操做先行于后面发生的这个变量的读操做,时间顺序ide

定义一个静态变量:static int a = 1;
线程A工做内存 | 线程B工做内存 | 指向 | 主内存 | 操做
-- | -- | -- | -- | --
-- | -- | -- | a = 1 | --
a = 1 | -- | <-- | a = 1 | 线程A拷贝主内存变量副本
a = 3 | -- | -- | a = 1 | 线程A修改工做内存变量值
a = 3 | -- | --> | a = 1 | 线程A工做内存变量存储到主内存变量
a = 3 | a = 3 | <-- | a = 3 | volatile原则:主内存变量保存线程A工做内存变量操做在线程B工做内存读取主内存变量操做以前优化

指令重排序和内存屏障

指令重排序:JVM在编译Java代码的时候或者CPU在执行JVM字节码的时候,对现有的指令进行从新排序,目的是为了再不影响最终结果的前提下,优化程序的执行效率this

内存屏障:一种屏障指令,让CPU或比编译器对屏蔽指令以前和以后发出的内存操做执行一个排序约束。
编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。atom

非线程安全

public class VolatileTest implements Runnable {

    public static volatile int num;

    @Override
    public void run() {
        for (int i = 0; i < 1000; i++) {
            num++;
        }

    }

    public static void main(String[] args) {
        for(int i = 0; i < 100; i++) {
            VolatileTest t = new VolatileTest();
            Thread t0 = new Thread(t);
            t0.start();
        }
        System.out.println(num);
        
    }
}

这段代码的结果有可能不是100000,有可能小于100000。
由于num++不是原子操做

使用原则

  1. 运行结果并不依赖变量的当前值,或者可以确保只有单一的线程修改变量的值
  2. 变量不须要与其余的状态变量共同参与不变约束

3、原子性、可见性、有序性

原子性

一个操做是不可中断的。即便是在多个线程一块儿执行的时候,一个操做一旦开始,就不会被其它线程干扰

原子性保障

  1. synchronized:monitorenter和monitorexit指令
  2. atomic类型:底层为native方法
  3. 基本数据类型(long,double非原子协定除外)

可见性

当一个线程修改了共享变量的值,其余线程当即可知

可见性保障

  1. volatile:先行发生(happens-before)原则
  2. synchronized:对一个同步块unlock以前必须把工做内存变量同步到主内存中
  3. final:final修饰的字段在构造器中初始化完成后,而且构造器没有把this引用传递出去,其余线程中就能看见final字段值

有序性

程序执行的顺序按照代码的前后顺序执行,在Java内存模型中,容许编译器和处理器对指令进行重排序,可是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性

有序性保障

  1. volatile:先行发生(happens-before)原则
  2. synchronized:同一个时间只能有一个线程得到锁

先行发生(happens-before)原则

JMM中两项操做之间的偏序关系,若是操做A发生于操做B以前,操做A发生的影响能够被操做B观察到

单线程和正确同步的多线程的执行结果不会被改变

规则

若是两个操做不在下列规则中,虚拟机能够对其重排序

  1. 程序次序规则:在一个线程内按控制流顺序
  2. 管程锁定规则:锁的unlock操做先发生于后面同一个锁的lock操做
  3. volatile变量规则:一个volatile变量的写操做先行于后面发生的这个变量的读操做
  4. 线程启动规则:start()先发生于此线程的每个操做
  5. 线程终止规则:线程的全部操做都先发生于线程终止操做
  6. 线程中断规则:对线程interrupt()方法先行于被中断线程的代码检查到中断事件的发生
  7. 对象终结规则:一个对象初始化完成先发生于它的finalize()方法的开始
  8. 传递性:操做A在操做B前,操做B在操做C前,操做A必定在操做C前
相关文章
相关标签/搜索