《java并发编程的艺术》内存模型

线程之间的通信机制:共享内存和消息传递。

线程之间的同步(程序用于控制不同线程间操作的相对顺序的机制)
在共享内存中,程序员要显示指定某个方法或某段代码需要在线程之间互斥执行。
在消息传递中,消息发送必须在消息接收之前,同步式隐式的。

java并发采用的是共享内存模型。

在java中,所有实例域、静态域、数组元素都存储在堆内存中,堆内存在线程间共享。
局部变量、方法定义参数和异常处理器参数不会再线程间共享。

JMM

java线程间的通信有java内存模型(JMM)控制,JMM决定了一个线程对共享变量的写入何时对另一个线程可见。

从抽象角度来看:线程之间的共享变量存储在主内存,每个线程都有一个私有的本地内存(抽象的但实际不存在),本地内存存储了该线程以读/写共享变量的副本。

重排序:
1. 编译器优化的重排序。
2. 指令级优化的重排序。
3. 内存系统的重排序。

JMM中,禁止特定类型的编译器重排序,
(2,3统称处理器重排序)JMM的处理器重排序要求java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers)

现代的处理器使用写缓冲区保存向内存写入的数据。写缓冲区保证指令流水线持续运行,避免由于处理器停顿等待向内存写入数据的延迟。同时通过批处理的方式刷新写缓冲区,合并写缓冲区中对同一内存地址的多次写,减少对内存总线的占用。写缓冲区仅对所在处理器可用。

StoreLoad Barriers一种内存屏障类型。确保Store1数据对其他处理器可见(刷新到内存)先于Load2及后续所有装载指令的装载。

happens-before:在发生前
一个线程的每个操作,happens-before于该线程的任意后续操作。并不意味着前一个操作必须要在后一个操作前执行,happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前。
对一个锁的解锁,happens-before于随后对这个锁的加锁。
对一个volatile域的写,happens-before于任意后续对这个域的读。
传递性:A happens-before B,且 B happens-before C,那么 A happens-before C。

as-if-serial:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。故重排序不能对具有依赖关系的操作做重排序。

顺序一致性

当程序为正确同步时,可能存在数据竞争(在一个线程中写另一个中读同一个变量,且读写没有通过同步排序)。

如果程序是正确同步的,程序的执行将与顺序一致性内存模型中的执行结果相同。

顺序一致性内存模型有两大特性:
1. 一个线程中所有操作必须按照程序的顺序来执行
2. 所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中每个操作都必须原子执行且立刻对都有线程可见。

JMM不允许临界区内的代码越界。因此会在退出和进入临界区这两个关键时间点做特别处理,使得线程在这两个时间点具有与顺序一致性模型系统的内存视图。这种在边界区的重排序既提高了执行效率,又不会影响执行效果。

对于未同步或未正确同步的多线程程序,JMM只提供最小安全性:线程执行是读取到的值,要么是之前某个线程写入的值,要么是默认值(0,null,false)
为了实现最小安全性,JVM在堆上分配对象时,首先对内存空间进行清零,然后才会在上面分配对象(JVM内部会同步这两个操作)。
未同步的程序在JMM中执行时,整体上是无序的,执行结果无法预知

volatile内存语义

class VolatileExample{
    volatile long v1 = 0L;//使用volatile声明64位的long变量

    public void set(long l){ //写方法
        v1 = l;
    }

    public void getAndIncrement(){ //自增方法
        v1++;
    }

    public long get(){ //读方法
        return v1;
    }
}


语义上相当于:

class VolatileExample{
    long v1 = 0L;

    public synchronized void set(long l){
        v1 = l;
    }

    public synchronized long get(){
        return v1;
    }

    public void getAndIncrement(){
        long temp = get();
        temp += 1L;
        set(temp);
    }
}

volatile自身具有以下特性:
- 可见性。对一个volatile变量的读,总数能看到任意线程对这个volatile变量最后的写入。
- 原子性。对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。

从内存语义的角度来说:
volatile的写-读 和 锁的释放-获取有相同的内存效果。

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

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

    public void read(){
        if(flag){       //3
            int i = a;  //4
            ......
        }
    }
}

假设线程A执行write()方法之后,线程B执行read()方法
happens-before关系分为3类:
1. 根据程序次序规则, 1 happens-before 2 ; 3 happens-before 4
2. 根据volatile规则,先写后读 2 happens-before 3
3. 根据happens-before的传递规则, 1 happens-before 4

即A线程在写volatile变量前所有可见的共享变量,在B线程读同一个volatile变量后,将立即变得对B线程可见。

当写一个volatile变量时,JMM会把该线程对应本地内存中的共享变量刷新到主内存中。

当读一个volatile变量时,JMM会把改线程对应的本地内存置为无效,从主内存中读取共享变量。

线程A写一个volatile变量,实际上是线程A对接下来将要读这个volatile变量的某个线程发出了(其对共享变量所做修改)消息。
线程B读一个volatile变量,实际上是线程B接收了之前某个线程发出(在写这个volatile变量之前对共享变量所做修改的)的消息
线程A写一个volatile变量,随后线程B读这个volatile变量,实际上是线程A通过主内存向线程B发送消息。

基于保守策略的JMM内存屏障插入策略:
在每个volatile写操作前面插入一个StoreStore屏障,后面插入一个StoreLoad屏障
在每个volatile读操作后面插入一个LoadLoad屏障,后面插入一个LoadStore屏障(都是后面)

StoreStore保证在volatile写之前,其前面的所有普通写操作已经对任意处理器可见(刷新到主内存)
StoreLoad避免volatile写与后面可能有的volatile读/写操作重排序
LoadLoad禁止处理器把上面的volatile读和下面的普通读重排序
LoadStore禁止处理器把上面的volatile读和下面的普通写重排序

举例:

class VolatileBarrierExample{
    int a;
    volatile int v1 = 1;
    volatile int v2 = 2;

    void readAndWrite(){
        int i = v1;  //第一个volatile读
        int j = v2;  //第二个volatile读
        a = i + j;   //普通写
        v1 = i + 1;  //第一个volatile写
        v2 = j * 2;  //第二个volatile写
    }
}

编译器在生成字节码时优化为:(注意部分屏障可以省略,这个是最简单的版本)

第一个volatile读 –> LoadLoad屏障 –> 第二个volatile读 –> LoadStore屏障 –> 普通写 –> StoreStore屏障 –> 第一个volatile写 –> StoreStore屏障 –> 第二个volatile写 –> StoreLoad屏障

不省略的版本:
第一个volatile读 –> LoadLoad屏障 –> LoadStore屏障 –> 第二个volatile读 –> LoadLoad屏障 –> LoadStore屏障 –> 普通写 –> StoreStore屏障 –> 第一个volatile写 –> StoreStore屏障 –> StoreLoad屏障 –> 第二个volatile写 –> StoreLoad屏障

锁的内存语义

锁除了让临界区互斥执行外,还可以让释放锁的线程向获取同一个锁的线程发送消息。

class MonitorExample{
    int a = 0;
    public synchronized void writer(){ //1
        a++;                           //2
    }                                  //3
    public synchronized void reader(){ //4
        int i = a;                     //5
        ....
    }                                  //6
}

假设线程A执行writer(),线程B执行reader()。
根据happens-before规则:(这里1 happens-before 2,简写成 1 - 2)
程序次序:1-2,2-3,4-5,5-6
加锁解锁:3-4
传递性:2-5
线程A在释放锁之前所有可见的共享变量,在线程B获取同一个锁之后,将立刻变得对B线程可变。

当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。
当线程获取锁时,JMM会把该线程对应的本地内存置为无效。临界区代码必须从主内存中读取共享变量。
和volatile一样。

ReentrantLock中,使用lock()获取锁,使用unlock()释放锁。
实现依赖于java同步器框架AbstractQueuedSynchronizer(简称AQS),AQS使用一个整型的volatile变量(命名为state)来维护同步状态。
在公平锁中,加锁方法先读volatile变量state,在释放锁的最后,写volatile变量state。
在非公平锁中,使用compareAndSet()方法 以原子操作的方式更新state变量。

由于java的compareAndSet具有volatile读和volatile写的内存语义,因此java线程之间的通信有以下4种方式:
- A线程写volatile变量,随后B线程读这个volatile变量
- A线程写volatile变量,随后B线程用CAS更新这个volatile变量
- A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量
- A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量

concurrent包的实现

首先,声明共享变量为volatile,
然后,使用CAS的原子条件更新实现线程之间的同步,
同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信

final域的内存语义

对于final域,编译器是处理器要遵守两个重排序规则:
1. 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,与这两个操作之间不能重排序。
2. 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。

举例:

public class FinalExample{
    int i;                        //普通变量
    final int j;                  //final变量
    static FinalExample obj;      

    public FinalExample(){        //构造函数
        i = 1;                    //写普通域
        j = 2;                    //写final域
    }

    public static void writer(){  //写线程A执行
        obj = new FinalExample();
    }

    public static void reader(){  //读线程B执行
        FinalExample object = obj;//读对象引用
        int a = object.i;         //读普通域
        int b = object.j;         //读final域
    }
}

假设线程A执行writer()方法,随后线程B执行reader()方法。

写final域的重排序禁止把final域的写重排序到构造函数之外:
JMM禁止编译器吧final域的写重排序到构造函数之外
编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障,禁止处理器把final域的写重排序到构造函数之外。

读final域的重排序规则是,在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作。编译器会在读final域操作前面插入一个LoadLoad屏障。

如果final域是引用类型,编译器和处理器会遵守以下约束:
在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造函数引用赋值给一个引用变量,这两个操作不能重排序

happens-befor

JMM把happens-before要求禁止的重排序分为了下面两类:
会改变程序执行结果的重排序。(禁止)
不会改变程序执行结果的重排序。(允许)

补充:
start规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作 happens-before 于线程B中的任意操作。
join规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作 happens-before于线程A从ThreadB.join()操作成功返回。

双重检查锁定和延迟初始化

在java多线程程序中,降低初始化类和创建对象的开销可以采用:延迟初始化。
双重检查锁定是常见的延迟初始化技术,但它是一个错误的用法!

public class UnsafeLazyInitialization{
    private static Instance instance;
    public static Instance getInstance(){
        if (instance == null){            //1:A线程执行
            instance = new Instance();    //2:B线程执行
        }
        return instance;
    }
}

假设A线程执行代码1的同时,B线程执行代码2.此时线程A可能会看到instance引用的对象还没有完成初始化。
所以我们在getInstance方法前加锁,即:public synchronized static Instance getInstance().该操作加锁然后会增加性能开销。
早期用双重检查锁定来降低开销。

public class DoubleCheckedLocking {
    private static Instance instance;
    public static Instance getInstance(){
        if (instance == null){
            synchronized(DoubleCheckedLocking.class){
                if(instance == null){
                    instance = new Instance();   //7
                }
            }
        }
        return instance;
    }
}

如果第一次检查instance不为null,那么就不需要执行下面的加锁和初始化操作,因此可以大幅降低synchronized的性能开销。
多个线程试图在同一时间创建对象时,会通过加锁来保证只有一个线程能创建对象。
在对象创建好之后,执行getInstance()方法将不需要获得锁,直接返回创建好的对象。

但是,双重检查锁定是有问题的,主要是第7行等价于以下伪代码:

memory = allocate(); //1分配对象内存空间
ctorInstance(memory);//2初始化对象
instance = memory;   //3设置instance指向刚分配的内存地址。

实际上,2和3有可能会出现重排序的情况。
所以有两个解决方案:
1. 不允许2和3重排序
2. 允许2和3重排序,但是不允许其他线程“看到”这个重排序

1.基于volatile的解决方案:将instance声明为volatile
2.基于类初始化的解决方案:

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

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

猜你喜欢

转载自blog.csdn.net/saywhat_sayhello/article/details/81075093
今日推荐