zoukankan      html  css  js  c++  java
  • 并发编程(四):内存语义

    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域在构造函数中被初始化之后的值

  • 相关阅读:
    HDU 2089 不要62
    HDU 5038 Grade(分级)
    FZU 2105 Digits Count(位数计算)
    FZU 2218 Simple String Problem(简单字符串问题)
    FZU 2221 RunningMan(跑男)
    FZU 2216 The Longest Straight(最长直道)
    FZU 2212 Super Mobile Charger(超级充电宝)
    FZU 2219 StarCraft(星际争霸)
    FZU 2213 Common Tangents(公切线)
    FZU 2215 Simple Polynomial Problem(简单多项式问题)
  • 原文地址:https://www.cnblogs.com/kenshine/p/14520364.html
Copyright © 2011-2022 走看看