并发编程(四):内存语义

1.volatile内存语义

Volatile主要作用是使变量在多个线程间可见

1.1 volatile特性

  • 可见性:对一个volatile变量的读,总能看到(任意线程)对该变量最后的写入
  • 原子性:即使是64为的long型和double型变量,只要声明为volatile变量,对该变量的读写就具有原子性volatile变量的复合操作不具有原子性,如volatile++

1.2 volatile写-读的内存语义

volatile写和锁的释放有相同的内存语义,volatile的读和锁的获取有相同的内存语义

volatile写的内存语义:

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

volatile读的内存语义:

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

1.3 volatile内存语义的实现

为了实现volatile语义,JMM会限制重排序(编译器,处理器),volatile重排序规则表如下(无数据依赖性):

  • 不管volatile读后面的操作是啥,都不能重排序

  • 不管volatile写前面的操作是啥,都不能重排序

  • volatile读写不管顺序如何都不能重排序

JMM保守实现策略(可根据不同处理器优化):

序号 位置 插入屏障
1 每个volatile写前 写写(Store-Store)屏障
2 每个volatile写后 写读屏障
3 每个volatile读后 读读屏障,读写屏障

第二条可以替换为每个volatile读前插入,但是这样替换会导致效率变低(一写多读)

实际执行时,只要不改变volatile写-读的内存语义,可以省略一些不必要的屏障

X86处理器仅会对写读重排序,所以JMM只需要在最后一个volatile写之后插入写-读屏障,其余屏障都会省略,所以x86处理器中volatile写的开销比读的开销大

1.4 volatile内存语义增强

JSR-133(jdk1.5)前允许volatile变量操作和普通变量操作重排序,无法保证数据的安全性

JSR-133后禁止了这种排序,确保了volatile的写-读和锁的释-放获取具有相同的内存语义

2. 锁的内存语义

volatile仅仅保证单个volatile变量的读写具有原子性,锁的互斥特性可以保证整个临界区代码的执行具有原子性

功能上,锁更加强大;在可伸缩性和性能上,volatile更具有优势。

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

当线程释放锁时,JMM会把线程对应的本地内存中的共享变量刷新到主存中

当线程获取锁时,JMM会把线程对应的本地内存置为无效,从而使被监视器保护的临界区代码必须从主存中读取共享变量

A线程释放锁,B线程获取同一个锁,相当于A线程通过主存向B线程发送消息

2.2 锁内存语义的实现

ReetrantLock类图(部分)如下:

AQS:AbstractQueueSynchronized,Java同步器框架

公平锁,线程获取锁顺序按照线程加锁的顺序来分配

非公平锁,获取锁抢占机制,随机获取锁

2.2.1 公平锁加锁

公平锁加锁调用轨迹如下:

  1. ReentrantLock:lock()
  2. FairSync:lock()
  3. AQS:acquire(int arg)
  4. ReentrantLock:tryAcquire(int acquires),真正开始加锁

tryAcquire方法部分代码如下:

    protected final boolean tryAcquire(int acquires) {
          final Thread current = Thread.currentThread();
          int c = getState();		//公平锁加锁方法首先获取volatile变量
          //其他操作
          return;
    }

2.2.2 解锁(公平/非公平)

解锁unlock的调用轨迹如下:

  1. ReentrantLock:unlock()
  2. AQS:release(int arg)
  3. Sync:tryRelease(int releases)

tryRelease方法部分代码如下:

    protected final boolean tryRelease(int releases) {
        int c = getState() - releases;
        //其他操作
        setState(c);		//解锁的最后写volatile变量
        return ;
    }

公平锁的获取和释放通过操作volatile变量实现

编译器和处理器不会对volatile写与写之前,volatile读与读之后的代码重排序

2.2.3 非公平锁加锁

非公平锁加锁调用轨迹如下:

  1. ReentrantLock:lock()
  2. NonfairSync:lock()
  3. AQS:compareAndSetState(int expect,int update)

compareAndSetState方法代码如下:

    protected final boolean compareAndSetState(int expect, int update) {	//CAS操作
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);	
    }

当状态值等于预期值,则以原子方式将同步状态设置为给定的更新值,CAS同时具有volatile读和写的内存语义

2.2.4 CAS内存语义

编译器和处理器不能对CAS和CAS前面或后面的任意内存操作重排序,同时具有volatile读和写的内存语义

程序会根据当前处理器的类型决定是否为cmpxchg指令添加lock前缀:

  • 多处理器,加上lock前缀,(Lock Cmpxchg)
  • 单处理器,省略lock前缀(单处理器具有顺序一致性)

lock前缀的作用如下:

  1. 确保对内存读-改-写操作的原子性,一些处理器会使用总线锁定,目前更多的使用缓存锁定
  2. 禁止该指令和之前之后的读和写指令重排序
  3. 把写缓冲区的所有数据刷新到内存中

2.2.5 总结

  • 公平锁和非公平锁释放,最后都要写一个volatile变量
  • 公平锁获取时,首先会去读volatile变量
  • 非公平锁获取时,首先会用CAS更新volatile变量

锁的内存语义实现至少有两种方式:

  1. 利用volatile变量的写-读所具有的内存语义
  2. 利用CAS所附带的volatile读和volatile写的内存语义

2.3 concurrent包的实现

CAS同时具有volatile读和写的语义,因此线程有四种通信方式:

  • A线程写volatile变量,B线程随后读这个volatile变量
  • A线程写volatile变量,随后B线程用CAS更新这个volatile变量
  • A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量
  • A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量

JUC包的通用实现方式:

  1. 声明变量为volatile
  2. 使用CAS的原子条件更新来实现线程之间的同步
  3. 配合volatile的读/写和CAS所具有的volatile读写的语义实现线程的通信

实现示意图:

3. final域的内存语义

与锁和volatile相比,对final域的读写更像是普通的变量访问

3.1 final域重排序规则(语义)

  1. 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序

  2. 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序

3.2 写final域重排序规则

禁止把final域的写重排序到构造函数之外

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

写final域重排序可以确保——在对象引用对任意线程可见之前,对象的final域已经被正确初始化过了,普通域没有这个保证

3.3 读final域重排序规则

在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM会禁止重排序这两种操作

读final域重排序可以确保——在读一个对象的final域之前,一定会先读包含这个final域的对象的引用

3.4 final域为引用类型

在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作间不能重排序

public class Example{
    final int[] array;
    static Example obj;
    
    public Example(){
        array =new int[1];array[0]=1;		//操作1
    }
    public static void write(){
        obj=new Example();					//操作2
    }
    //其他操作
}

3.5 final引用不能“溢出”

在构造函数内部,不能让这个被构造对象的引用为其他线程所见,也就是对象引用不能在构造函数中“溢出”

public class Example2{
    final int i;
    static Example2 obj;
    public Example2(){
        i=1;	
        obj=this;			//这一步会重排序导致“溢出”
    }
    //其他操作
}

3.6 final语义的实现

(编译器)在final写之后,构造函数return之前插入一个写写(StoreStore)屏障

在读final域的操作前面插入一个读读(LoadLoad)屏障

X86处理器中,final域的读/写不会插入任何内存屏障,X86可以保证这些操作不重排序

  • 写写不会重排序(写内存语义)
  • 有间接数据依赖的不会重排序(读内存语义)

3.7 final增强语义

JSR-133增强了final语义,提供了初始化安全保证:

只要对象是正确构造的(无逸出),那么不需要使用同步就可以保证在任意线程都能看到这个final域在构造函数中被初始化之后的值

posted @ 2021-03-11 20:35  菜鸟kenshine  阅读(246)  评论(0编辑  收藏  举报