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

  • 相关阅读:
    LR通过SiteScope监控mysql
    安装apk文件报waiting for device 时解决办法
    VMware虚拟机中设置端口映射(转载)
    Android模拟器中安装APK文件(转)
    netstat监控大量ESTABLISHED连接与Time_Wait连接问题(转载)
    linux 安装sysstat使用iostat、mpstat、sar、sa(转载)
    SQL联合查询中的关键语法(转)
    JVM调优总结 -Xms -Xmx -Xmn -Xss(转载)
    JVM系列三:JVM参数设置、分析(转载)
    查找进程并杀进程的命令
  • 原文地址:https://www.cnblogs.com/kenshine/p/14520364.html
Copyright © 2011-2022 走看看