并发编程(一) -- volatile

·  阅读 306

一:摘要概述

  • 1:JMM内存模型
  • 2:happens-before规则
  • 3:volatile内存可见保证
  • 4:禁止指令重排序
  • 5:JDK源码应用分析
  • 6:简单实践系列

二:JMM内存模型

JMM并非是JVM,这一点需要开篇前提醒诸位。从内存不安全的角度简单理解如下:个人不喜大篇幅理论复制,容易睡着

2.1 设计缘由

现代计算机CPU处理速度远远快于主内存处理速度,为解决亦或是缓解该情况提出本地工作内存概念

2.2 问题爆发

本地工作内存与主内存并非实时同步,当线程AB读取共享变量num到工作内存,线程A修改num变量值,此时线程B依然会读取到自身工作内存的num副本

三:happens-before规则

简单理解可以描述为A优先于B执行,那么A的操作结果B可见,这是JMM给出的保证。happens-before具有传递性,即如下图所示:A优先于B,B优先于C,所以A优先于C

四:内存可见性

工作内存的设计会导致线程并发不安全问题,volatile通过happens-before规则保证内存可见性。即volatile写happens-before所有对这块内存操作的volatile读,当然具体操作就是在volatile修饰的共享变量执行写操作时会被添加lock指令。该指令主要有以下两方面影响:

  • 1:将当前处理器缓存行的数据写回系统内存
  • 2:这个写回内存错的操作使得其它工作内存对于该变量的缓存失效

五:指令重排序

JMM在不改变单线程正确语义的前提下,允许编译器和处理器对指令进行重排序,比如经常有人举例的双重锁校验时会出现的问题

5.1 重排序效果

指令没有重排序之前可能一个对象的初始化过程如下:

  • 1:分配内存空间
  • 2:初始化对象
  • 3:设置对象引用指向分配内存空间

经过指令重排序后过程如下,但是单线程环境下是没有影响的

  • 1:分配内存空间
  • 3:设置对象引用指向分配内存空间
  • 2:初始化对象

5.2 双重锁重排序问题

先看一段双重锁实现单例模式的经典代码:为synchronized的存在保证了线程安全,想象与愿景是美好的

Object lock = new Object();
Object o = null;
if(o == null){
    synchronized(lock){
        if(o == null){
            o = new Object();
        }
    }
}
复制代码

重排序问题出在哪?如果线程A执行完o=new Object()但是注意执行完的是上面讲解的指令重排序后的第三步,也就是还没有真正意义上的初始化对象。这时线程B执行第一个判断,发现o非空拿着o的引用去用必然异常。过程如下图所示:

时间 线程A 线程B
T1 1:分配内存
T2 2:引用指向内存地址
T3 3:判断引用非空
T4 4:直接使用对象引用
T5 6:对象初始化 5:异常

5.2 禁止指令重排序

禁止指令重排序需要添加内存屏障,当然如果是volatile修饰共享变量读写操作都会默认添加内存屏障保障操作不会被重排序。具体操作如下:图片来源于并发编程的艺术(绝对大师级别的著作)

六:JDK源码应用

JDK源码中ConcurrentHashMap绝对是需要精读的一个类,它的并发安全利用的是volatile + cas + synchronized实现。其中get()函数就是通过volatile保证的线程安全。查看一下内部类Node节点如下代码:val节点值,next链表指针都被volatile修饰,根据happens-before如果有线程修改该节点,那么其结果对于读线程必须可见。即get()函数不需要加锁也能利用volatile保证内存可见性的特点实现线程安全

    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V val;
        volatile Node<K,V> next;
复制代码

七:简单实践

A线程与B线程的生产-消费模式,交互打印A和B。场景分析一下就是控制线程执行,这就涉及到线程通信,简单粗暴就是synchronized + wait + notify。简单想想或许volatile就可以实现:

    private static volatile  boolean flag = false;
    
    public static void main (String[] args) {
        
        Thread threadA = new Thread(() -> {
            for (;;) {
                if (!flag) {
                    System.out.println("A");
                    flag = true;
                }
            }
        });
        
        Thread threadB = new Thread(() -> {
           for(;;){
               if (flag){
                   System.out.println("B");
                   flag = false;
               }
           }
        });
        threadA.start();
        threadB.start();
复制代码
分类:
后端
标签:
收藏成功!
已添加到「」, 点击更改