玩转高并发系列----多线程基础(二)

volatile关键字

  1. 多线程中的三个重要特性:
  1. 原子性:指的是一个读/写操作可以当做是一个原子操作。如赋值操作等。

注意点:JVM的规范并没有要求64位的long或者double的写入是原子的。因此,在32位的机器上,一个64位变量的写入可能被拆分成两个32位的写操作来执行。这样可能读线程只能读到写线程写入的“一半”的值。

  1. 内存可见性:指的是“写完之后立即对其他线程可见”,它的反面不是“不可见”,而是“稍后才能见”,也就是volatile保证了强一致性。

如上一节的示例代码中,线程关闭的标志位flag,它是一个boolean类型的数字,可能会出现主线程把它设置成了false,而工作线程读到的却还是false的情形。

  1. 重排序:即程序实际执行的指令顺序和代码中的顺序不是完全一致。主要有以下几种重排序:
  • 编译器重排序:对于没有先后依赖关系的语句,编译器可以重新调整语句的执行顺序。
  • CPU指令重排序:在指令级别,让没有依赖关系的多条指令并行。
  • CPU内存重排序:CPU有自己的缓存,指令的执行顺序和写入主内存的顺序不完全一致。

如下示例是经典的双重检查单例模式,但是存在指令重排序的问题。

/*单例程序*/
public class SingleObject{
     
    private static SingleObject INSTANCE;
    private SingleObject(){
     
        
    }
    
    public static SingleObject getInstance(){
     
        if (null == INSTANCE){
     
            synchronized (SingleObject.class){
     
                if (null == INSTANCE){
     
                    INSTANCE = new SingleObject();
                }
            }
        }
        return INSTANCE;
    }
}

以上程序的INSTANCE=new SingleObject();语句可以分为以下三个阶段

  1. 为SingleObject对象分配一块内存。
  2. 在分配的内存中,初始化SingleObject对象的一系列成员变量。
  3. 将INSTANCE引用指向该内存地址。

但是,由于步骤2和步骤3二者之间没有先后的依赖关系,因此jrt或者CPU处于性能考虑,可能会对步骤2和步骤3进行重排序,即步骤3可能在步骤2之前完成。此时,另一个线程就可能拿到一个还没有初始化完成的SingleObject对象。

  1. volatile关键字的作用:
    1. 64位写入的原子性。
    2. 内存可见性。
    3. 禁止重排序。

happen-before

  1. as-if-serial语义:即线程看起来是完全串行的。

1 单线程程序的重排序语义:不管怎么重排序,单线程程序的执行结果不能改变。换句话说,就是只要操作之间没有数据依赖性,编译器和CPU都可以任意重排序,因为执行结果不会改变,代码看起来就像是完全串行地一行行从头执行到尾。这就是as-if-serial语义。

2.多线程程序的重排序语义:编译器和CPU只能保证每个线程内部都是“看似完全串行的”,但多个线程会互相读取和写入共享的变量,对于这种相互影响,编译器和CPU不会考虑。

玩转高并发系列----多线程基础(二)_第1张图片
2. happen-before的定义
JMM引入了happen-before,使用happen-before描述两个操作之间的内存可见性。例如,A happen-before B,意味着A的执行结果必须对B可见,也就是保证跨线程的内存可见性。
注意:A happen-before B不代表A一定在B之前执行,因为对于多线程程序而言,两个操作的执行顺序是不确定的。happen-before只确保如果A在B之前执行,则A的执行结果必须对B可见。
3. happen-before具有传递性:即A happen-before B,B happen-before C,则A happen-before C。

Java中的volatile关键字不仅仅具有内存可见性,还会禁止volatile变量写入和非volatile变量写入的重排序。


内存屏障

  1. 什么是内存屏障?

为了禁止编译器重排序和CPU重排序,在编译器和CPU层面设置了对应的指令,这些指令就是内存屏障。内存屏障是happen-before的底层实现原理。

  1. 内存屏障分类:在理论层面,可以把基本的CPU内存屏障分为四种:
  1. LoadLoad: 禁止读和读的重排序
  2. StoreLoad:禁止写和读的重排序
  3. LoadStore:禁止读和写的重排序
  4. StoreStore:禁止写和写的重排序

JDK中的UnSafe提供了三个内存屏障native方法:

public final class UnSafe{
     
......
    public native void loadFence();
    public native void storeFence();
    public native void fullFence();
......
}
  1. loadFence() ======== LoadLoad + LoadStore
  2. storeFence() ======= StoreStore + LoadStore
  3. fullFence()========= LoadLoad + LoadStore + StoreLoad + StoreStore
  1. volatile实现原理:
  1. 在volatile写操作的前面插入一个StoreStore屏障。保证volatile写操作不会和之前的写操作重排序。
  2. 在volatile写操作的后面插入一个StoreLoad屏障。保证volatile写操作不会和之后的读操作重排序。
  3. 在volatile读操作的后面插入一个LoadLoad+LoadStore屏障。保证volatole读操作不会和之后的读操作,写操作重排序。
  1. final的happen-before语义:
  1. 对final域的写(构造函数内部),happen-before于后续对final域所在对象的读。
  2. 对final域所在对象的读,happen-before于后续对final的读。
    通过这种happen-before语义的限定,保证了final域的赋值,一定在构造函数之前完成,不会出现另外一个线程读取到了对象,但对象里面的变量却还没有初始化的情形,避免出现构造函数溢出的问题。
  1. 常用的happen-before规则:
  1. 单线程中的每个操作,happen-before对应该线程中任意后续操作。
  2. 对volatile变量的写入,happen-before对应后续对这个变量的读取。
  3. 对synchronized的解锁,happen-before对应后续对这个锁的加锁。
  4. 对final变量的写,happen-before与final域对象的读,happen-before于后续对final变量的读。

你可能感兴趣的:(Java高并发,java,多线程)