Java并发编程艺术(二) Java内存模型

1、Java内存模型基础

1.1 并发编程模型的两个关键问题

线程之间如何通信、线程之间如何同步。

1.2 Java内存模型的抽象结构

  • 所有实例域、静态域和数组元素都存储在堆内存中,堆内存在线程之间共享。
  • 局部变量、方法定义参数和异常处理器参数不会再线程之间共享。
  • Java线程之间通信由Java内存模型控制。线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读写共享变量的副本。本地内存是抽象概念,并不真实存在。
Java内存模型抽象结构

1.3 源代码到指令序列的重排序

排序
  • 2、3是处理器排序,会有一些内存屏障来禁止一些重排序。

1.4 并发编程模型分类

  • 处理器对内存的读写操作执行顺序,不一定与内存实际发生的读写操作顺序一致。

  • 下面两张图展现的就是在没刷写之前读了,最后结果不一样

1

2
处理器重排规则
四种内存屏障

1.5 happens-before

  • 如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。
hapens-before与JMM

2、重排序

2.1 数据依赖性

  • 如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。

2.2 as-if-serial语义

  • 不管怎么重排序,单线程程序执行的结果不能被改变。

2.3 重排序对多线程的影响

class ReorderExample {
    int     a    = 0;
    boolean flag = false;

    public void writer() {
        a = 1; //1
        flag = true; //2
    }

    public void reader() {
        if (flag) { //3
            int i = a * a; //4
            //s¡­¡­
        }
    }
}
  • 这里面1和2没有数据依赖关系,如果两个线程分别执行writer和reader就有可能出问题。
程序执行
  • 如果把writer和reader改成synchronized方法。
synchronized程序执行

3、volatile内存语义

3.1 volatile特性

  • 一个volatile变量的单个读写操作,与一个普通变量的读写操作都是使用同一个锁来同步,他们之间的执行效果相同。
  • volatie变量自身具有可见性与原子性。

3.2 volatile写-读建立的happens-before关系

class VolatileExample {
    int              a    = 0;
    volatile boolean flag = false;

    public void writer() {
        a = 1; //1
        flag = true; //2
    }

    public void reader() {
        if (flag) { //3
            int i = a; //4
            //¡­¡­
        }
    }
}
  • 2和3是有happens-before关系
volatile happens-befor

3.3 写-读内存语义

  • 当写一个volatile变量,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。
  • 当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程截下来将从主内存中读取共享变量。

3.4 volatile内存语义的实现

volatile重排序规则表
  • 当第二个操作是写时,不论第一个操作是什么,都不能重排序。
  • 当第一个操作是volatile读,不管第二个操作是什么都不能重排序。
  • 当第一个操作是写,第二个是读不能重排序。

基于保守策略的JMM内存屏障:

  • 在每个volatile写操作前插入StoreStore屏障
  • 在每个volatile写操作后插入StoreLoad屏障
  • 在每个volatile读操作后面插入一个LoadLoad屏障
  • 在每个volatile读操作后面插入一个LoadStore屏障
volatile写

volatile读

4、锁的内存语义

4.1 锁的释放获取建立的happens-before关系

class MonitorExample {
    int a = 0;

    public synchronized void writer() { //1
        a++; //2
    } //3

    public synchronized void reader() { //4
        int i = a; //5
        //¡­¡­
    } //6
}
锁 happens-before

4.2 锁的释放和获取的内存语义

  • 当线程获取锁时,JMM会把该线程对应的本地内存置为无效。
  • 当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。

4.3 锁内存语义的实现

class ReentrantLockExample {
    int           a    = 0;
    ReentrantLock lock = new ReentrantLock();

    public void writer() {
        lock.lock(); 
        try {
            a++;
        } finally {
            lock.unlock(); 
        }
    }

    public void reader() {
        lock.lock(); 
        try {
            int i = a;
            //¡­¡­
        } finally {
            lock.unlock();
        }
    }
}
ReentrantLock类图
  • ReentrantLock: lock()
  • FairSync: lock()
  • AbstractQueuedSynchronizer: acquire(int arg)
  • ReentrantLock: tryAcquire(int acquires)
tryAcquire

5、final域的内存语义

5.1 final域的重排序规则

  • 在构造函数内对一个final域的写入,与随后吧这个被构造对象引用赋值给一个引用变量,不能重排序。
  • 除此读一个包含final域的对象引用,与随后初次读这个final域,不能重排序。
public class FinalExample {
    int                 i; 
    final int           j;  
    static FinalExample obj;

    public FinalExample() {
        i = 1; 
        j = 2; 
    }

    public static void writer() { 
        obj = new FinalExample();
    }

    public static void reader() { 
        FinalExample object = obj; 
        int a = object.i; 
        int b = object.j; 
    }
}

5.2 写final域的重排序规则

  • JMM禁止编译器把final域的写重排序到构造函数之外。
  • 编译器会在final域的写之后,构造函数return之前插入一个StoreStore屏障。这个屏障禁止处理器把final域的写重排序到构造函数之外。
  • 这样可以保证对象引用为任意线程可见之前,对象final域已经被正确初始化过。比如下图普通变量i可能没初始化就被读了。
final写

5.3 读final域的重排序规则

  • 初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作。
  • 在读一个对象的final域之前,一定会先读包含这个final域的对象的引用。
final读

6、happens-before

  • 如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
  • 并不意味Java平台的具体实现必须要按照happens-before关系指定的顺序来执行,如果重排序结果和按happens-before结果一直,那么这种重排序并不非法。

规则

  • 程序顺序规则
  • 监视器锁规则:锁的释放 happens-before随后对这个锁的加锁。
  • volatile变量规则
  • 传递性
  • start()规则,如果线程A执行ThreadB.start(),那么线程A的ThreadB.start()操作happens-beforeB中任意操作。
  • join()规则,如果A执行ThreadB.join()并成功返回,那么线程B中任意操作happens-before于线程A从ThreadB.join()操作成功返回。
join规则

7、双重检查锁定与延迟初始化

public class DoubleCheckedLocking { //1
    private static Instance instance; //2

    public static Instance getInstance() { //3
        if (instance == null) { //4
            synchronized (DoubleCheckedLocking.class) { //5
                if (instance == null) //6
                    instance = new Instance(); //7
            } //8
        } //9
        return instance; //10
    } //11

    static class Instance {
    }
}
  • 第七行执行的是三个步骤,第二步和第三步可能重排序,导致另外一个线程可以在执行到第四步时访问到一个未初始化的对象。
memory = allocate();  // 分配空间
ctorInstance(memory); // 初始化对象
instance = memory; // 设置instance指向刚分配的内存地址
执行顺序

7.1 基于volatile解决

  • 因为volatile写后面有StoreLoad屏障。
public class SafeDoubleCheckedLocking {
    private volatile static Instance instance;

    public static Instance getInstance() {
        if (instance == null) {
            synchronized (SafeDoubleCheckedLocking.class) {
                if (instance == null)
                    instance = new Instance();
            }
        }
        return instance;
    }

    static class Instance {
    }
}

7.2 基于类初识化

public class InstanceFactory {
    private static class InstanceHolder {
        public static Instance instance = new Instance();
    }

    public static Instance getInstance() {
        return InstanceHolder.instance; 
    }

    static class Instance {
    }
}
类初始化

类初始化过程,竞争的类会读取到state,拿到初始化锁的那个类会修改这个值。

第1阶段

第2阶段

第4阶段

你可能感兴趣的:(Java并发编程艺术(二) Java内存模型)