Java并发编程之并发关键字

volatile

保证可见性

  • 一个线程修改volatile变量的值时,该变量的新值会立即刷新到主内存中,这个新值对其他线程来说是立即可见的
  • 一个线程读取volatile变量的值时,该变量在本地内存中缓存无效,需要到主内存中读取
boolean stop = false;// 是否中断线程1标志
//Tread1
new Thread() {
    public void run() {
        while(!stop) {
          doSomething();
        }
    };
}.start();
//Tread2
new Thread() {
    public void run() {
        stop = true;
    };
}.start();

保证有序性

graph LR 任意操作-->|不能重排序|volatile写 volatile读-->|不能重排序|任意操作 volatile写-->|不能重排序|a[volatile读]

volatile写实际上是在前面加了一个StoreStore屏障,要求不能和前面的写操作进行重排序。读操作没有要求,只是因为volatile读后面加了LoadStore屏障,因此也不能和前面的volatile读重排序。至于普通读,应该是可以进行重排的,猜测是由于读取对数据本身不产生影响,多线程情况下不存在安全问题(很多资料中都写的是前面任意操作都不能进行重排序)

boolean inited = false;// 初始化完成标志
//线程1:初始化完成,设置inited=true
new Thread() {
    public void run() {
        context = loadContext();   // 语句1
        inited = true;             // 语句2
    };
}.start();
//线程2:每隔1s检查是否完成初始化,初始化完成之后执行doSomething方法
new Thread() {
    public void run() {
        while(!inited){ // 语句3
          Thread.sleep(1000);
        }
        doSomething(context);
    };
}.start();

线程1中,语句1和语句2之间不存在数据依赖关系,JMM允许这种重排序。如果在程序执行过程中发生重排序,先执行语句2后执行语句1,会发生什么情况?

当线程1先执行语句2时,配置并未加载,而inited=true设置初始化完成了。线程2执行时,读取到inited=true,直接执行doSomething方法,而此时配置未加载,程序执行就会有问题

不保证原子性

volatile是不能保证原子性的,可使用原子类或者加锁

volatile实现原理

  • 有序性原理
graph LR StoreStore屏障-->|前屏障|volatile写 volatile写-->|后屏障|StoreLoad屏障 volatile读-->|后屏障1|LoadLoad屏障 LoadLoad屏障-->|后屏障2|LoadStore屏障
  • 可见性原理
graph LR volatile写-->StoreLoad屏障 StoreLoad屏障-->|Lock前缀指令|volatile读

Lock前缀的指令将该变量所在缓存行的数据写回到主内存中,并使其他处理器中缓存了该变量内存地址的数据失效

当其他线程读取volatile修饰的变量时,本地内存中的缓存失效,就会到到主内存中读取最新的数据

  • 总线风暴

基于 CPU 缓存一致性协议,JVM 实现了 volatile 的可见性。但由于总线嗅探机制,会不断的监听总线。如果大量使用 volatile,cas不断循环无效交互会导致总线带宽达到峰值,引起总线风暴。

伪共享问题

public class Share {
   volatile int value;
}

volatile修饰解决了value内存可见性问题,但由于线程本地缓存是以缓存行为单位,可能会存储其他变量。因此,volatile带来的缓存失效会使同一缓存行上的其他变量也失效,访问时也需要从主从中再次获取,带来性能问题,此类问题称之为伪共享。

如何解决呢?

保证一个缓存行上只有一个share变量即可。早期版本JDK有使用无用变量作为填充物解决的,但是存在不同机器缓存行大小不一致、无用填充物被JVM优化掉等问题。基于此,在Java 8官方提供了Contended 注解,如下:

public class Share {
    @Contended
    volatile int value;
}

使用如上注解,需要在 JVM 启动参数中加入 -XX:-RestrictContended,这样 JVM 在运行时就会自动的为我们的 Share 类添加合适大小的填充物(padding)来解决伪共享问题。

final

基本特性

  • final变量只能被赋值一次,赋值后值不再改变(引用对象地址值不能改变,但内容可以变化)
  • final修饰的方法在编译阶段被静态绑定(static binding),不能被重写
  • final修饰的类不能被继承,所有成员方法都会被隐式地指定为final方法

并发final

graph LR final写-->StoreStore屏障 StoreStore屏障-->B[Store 对象引用]

final变量赋值必须在所属对象引用获取前完成,通过在final写后面插入StoreStore屏障,禁止处理器把final域的写重排序到构造函数之外

graph LR A[Load 对象引用]-->LoadLoad屏障 LoadLoad屏障-->final读

相反,final变量读取必须在所属对象引用获取后完成,通过在final读前面插入LoadLoad屏障,禁止读对象引用和读该对象final域重排序

public class FinalDemo {
    private int a;  // 普通域
    private final int b; // final域
    private static FinalDemo finalDemo;

    public FinalDemo() {
        a = 1; // ①写普通域
        b = 2; // ②写final域
    }

    // 线程A先执行writer()方法
    public static void writer() {
		 // 两个操作:
		 // 1)构造一个FinalExample类型的对象,①写普通域a=1,②写final域b=2
		 // 2)③把这个对象的引用赋值给引用变量finalDemo
        finalDemo = new FinalDemo();
    }

    // 线程B后执行reader()方法
    public static void reader() {
        FinalDemo demo = finalDemo; // ④读对象引用
        int a = demo.a;    // ⑤读普通域
        int b = demo.b;    // ⑥读final域
    }
}

若示例中final int a变为引用类型final int[] arrays,在构造函数中初始化数组并赋值,赋值语句同样不会重排序到构造函数以外

posted @ 2021-03-13 18:09  肆玖爺  阅读(98)  评论(0)    收藏  举报