多线程(8)- 深入volatile关键字


前言

并发编程三特性:原子性、有序性、可见性,volatile关键字
扩展:

概念

1、并发编程

1.1原子性

  • 概念:在一次的操作或者多次操作中,要么所有操作全部都起到执行,要么所有操作都不执行。
  • valitale不保证数据的原子性,synchronized保证,自JDK1.5版本起。其提供的原子类型变量也可以保证原子性。

1.2可见性

  • 概念:当一个线程对一个共享变量进行了修改,那么另外的线程可以立即看到修改后的最新值。

  • Reader线程会将x从主内存读取到CPU Cache中,也就是从主内存缓存到线程的本地内存中,Updater线程对x的修改对Reader是不可见的。

1.3有序性

  • 指令重排导致代码在运行时的指令执行顺序不一定是编码顺序
  • JVM在运行程序时会进行指令重排,处理器对输入的代码指令进行优化,如果一个指令x在执行过程中需要用到指令y的执行结果,那么处理器会保证y在x之前执行。
public class ThreeSpecialClass {

    public static final int init_value = 0;
    public static final int MAX        = 5;

    private boolean initialized = false;
    private Context context;

    public static void main(String[] args) {
        /***原子性:一个或多个操作不可分割 , 要么都成功要么都失败**/
        int x = 0;
        x++;// 实际是 x = 0   x+1   为x赋值x+1

        /**可见性:**/
        //因为CPU Cache模型原因, init_value会被加载到CPU Cache中,也就是线程的本地内存副本,导致线程读取的是副本的值,没有读取到主存的值
        new Thread(() -> {
            int local_value = init_value;
            while (local_value < MAX) {
                if (local_value != init_value) {
                    System.out.println(Thread.currentThread().getName() + " local_value =" + local_value + " x = " + init_value);
                    local_value = init_value;
                }
            }
        }, "reader").start();

        /**有序性:处理器为了提高效率会进行指令重排序 , 单线程不受影响,多线程并发时可能出现问题**/
        int a = 0;
        int b = 0;
        a++;
        b = 20;
    }

    public Context load() throws NamingException {
        if (!initialized) {
            /**有序性
             * 以下进行指令重排,先执行了initialized = true 再执行初始化,但在未执行初始化之前
             * 这时其它线程发现initialized为true而不去初始化context,直接返回context,此时context为null
             */
            context = new InitialContext();
            initialized = true;
        }
        return context;
    }
    
}

2、JVM保证三大特性

  • JVM-JMM满足一致内存访问效果
  • JMM规定所有的变量都是存在于主内存(RAM),每个线程都有自己的工作内存或本地内存(这点像CPU Cache),线程对变量的所有操作都必须在工作内存中进行,不能直接对主内存操作,且线程间的工作内存或本地内存不可访问。
  • 比如某个线程中对变量赋值 i=1; 必须在本地内存中修改i的值才能将其写入主内存中。

2.1JMM与原子性

  • x = 10 :原子性

  • y = x :非原子性

    1. 从主存读取x的值(如果当前执行线程工作内存中有x的值,则直接获取)
    2. 将执行线程工作内存中y的值修改为x的值
    3. 将y写入主内存
  • x ++ 等价于 x = x+ 1;非原子性

    1. 从主存读取x的值(如果当前执行线程工作内存中有x的值,则直接获取)
    2. 将执行线程工作内存中x的值加1操作
    3. 将x写入主内存
  • 多个原子性操作在一起不是原子性

  • 简单的读取与赋值是原子性,将一个变量赋值给另一个变量不是原子性

  • JMM只保证了基本读取和简单赋值是原子性

  • 如果想使得某个片段具备原子性,需要使用syncharonized、或JUC的Lock

  • 如果想要使得int类型自增操作具备原子性可使用JUC包下的原子封装类型java.util.concurrent.atomic.*

  • volatile不具备保证原子性语义

2.2JMM与可见性

  • Java提供了三种方法来提供可见性
    1. 使用关键字volatile,当一个变量被volatile修饰,对于共享变量的读操作会在主内存(当其他线程修改了该变量,会使该变量在其它线程的工作内存失效,所以必须从主内存获取),对于共享资源的写操作是先更新工作内存,修改结束后会立即刷新到主内存。
    2. 使用synchronized保证可见性,只有一个线程获取锁,还会确保在释放锁之前对变量的修改刷新到主内存中
    3. JUC提供的显示锁Lock也可以保证可见性,只有一个线程获取锁,还会确保在释放锁之前对变量的修改刷新到主内存中

2.3JMM与有序性

  • Java提供了三种保证有序性的方式

    • volatile
    • synchronized:同步机制
    • Lock:同步机制
  • happens-before原则:Java的内存模型具备一些天生的有序性规则,不需要任何同步手段就能保证有序性

    1. 程序次序规则:在同一个线程内,代码按照编写时的规则执行
    2. 锁定规则:一个unlock操作要先发生于对同一个锁的lock操作,意思是无论单线程还是多线程,如果一个锁是lock状态,那么必须释放锁才能继续进行lock操作(针对同一个锁对象)
    3. vilatile变量规则:对一个变量的写操作要早于对这个变量之后的读操作,如果两个线程,一个线程此时在写,另一个线程此时在读,那么必须是写操作先执行。
    4. 传递规则:如果操作A在操作B前,操作B在操作C前,那么操作A一定在操作C前。传递性
    5. 线程启动规则:Thread对象的start()方法先发生于该对象的任何操作,只有start之后才能真正运行。
    6. 线程中断规则:对线程执行interrupt()方法肯定要优先于捕获到中断信号,意思是如果线程收到了中断信号,那么此前一定执行过interrupt方法。
    7. 线程的终结规则:线程的所有操作要先发生于线程的终止检测,线程的执行一定在线程死亡前。
    8. 对象的终结规则:一个对象初始化的完成先行发生于finalize()方法之前。
      总结:volatile关键字具有保证顺序性的语义;

3、深入解析

3.1 volatile语义

  • 理解volatile保证可见性
  1. Reader从主存中读取init_value的值,加载到工作内存
  2. Updater将init_value加载到工作内存且修改init_value的值,并更新到主内存
  3. Reader工作内存中init_value的值失效
  4. 由于在工作内存中的值失效,再次从主存中读取
  • 理解volatile保证顺序性
int x = 0 ;
int y = 1;
volatile int z = 20; //volatile保证执行到这句时,上面x y已经赋值好了,同理x++和y--必须在z赋值之后才执行。
x++;
y--;

if (!initialized) {
            /**有序性
             * 以下进行指令重排,可能导致context为null ,先执行了initialized = true
             * 这时其它线程发现initialized为true而不去初始化context,导致context为null
             */
            context = new InitialContext();
            /**使用volatile修饰,保证在initialized = true时 上面代码已经执行完毕。
             * 这里必须对initialized加volatile,同时保证可见性,防止重复初始化
             * 对context加volatile可以保证content初始化在initialized=true之前执行,
             * 但是无法保证content初始化原子性,而且无法保证initialized对其它线程可见
             * */
            initialized = true;
        }
  • 理解volatile不保证原子性
    • i++ 操作对 i 使用volatile关键字
    • A线程读取主内存i的值到工作内存为0
    • A线程对i进行+1操作重新写到工作内存,但未写到主内存,此时CPU时间片切换到线程B
    • 因为A线程未写到主内存,所以线程B读取主内存i为0,现对i加1且更新到主内存,此时i=1
    • 线程切换A,A线程将工作内存i的值写入到主内存也为1
    • 造成AB两个线程都累加但是最终主内存结果为1
public class ThreeSpecialClass {

    public static final int init_value = 0;
    public static final int MAX        = 5;

    public volatile static int count = 0;
    private static boolean initialized = false;
    private static Context context;

    public static void main(String[] args) throws InterruptedException, NamingException {
        /***原子性:一个或多个操作不可分割 , 要么都成功要么都失败**/
        //atomic();
        /**可见性:**/
        //visiable();
        //有序性
        //orderliness();
        //volatile非原子性
        testVolatileNoAtomicity();
    }

    private static void atomic() {
        int x = 0;
        x++;// 实际是 x = 0   x+1   为x赋值x+1
    }

    private static void visiable() {
        //因为CPU Cache模型原因, init_value会被加载到CPU Cache中,也就是线程的本地内存副本,导致线程读取的是副本的值,没有读取到主存的值
        new Thread(() -> {
            int local_value = init_value;
            while (local_value < MAX) {
                if (local_value != init_value) {
                    System.out.println(Thread.currentThread().getName() + " local_value =" + local_value + " x = " + init_value);
                    local_value = init_value;
                }
            }
        }, "reader").start();
    }

    public static Context orderliness() throws NamingException {
        /**有序性:处理器为了提高效率会进行指令重排序 , 单线程不受影响,多线程并发时可能出现问题**/
        if (!initialized) {
            /**有序性
             * 以下进行指令重排,可能导致context为null ,先执行了initialized = true
             * 这时其它线程发现initialized为true而不去初始化context,导致context为null
             */
            context = new InitialContext();
            /**使用volatile修饰,保证在initialized = true时 上面代码已经执行完毕。
             * 这里必须对initialized加volatile,同时保证可见性,防止重复初始化
             * 对context加volatile可以保证content初始化在initialized=true之前执行,
             * 但是无法保证content初始化原子性,而且无法保证initialized对其它线程可见
             * */
            initialized = true;
        }
        return context;
    }

    public static void testVolatileNoAtomicity() throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(10);
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                for (int j = 0; j < 1000; j++) {
                    count++;
                }
                countDownLatch.countDown();
            }).start();
        }
        countDownLatch.await();
        System.out.println(count);
    }

}

3.2 volatile原理和实现机制

  • OpenJDK下unsafe.cpp源码的阅读,被volatile修饰的变量存在lock;的前缀,相当于一个内存屏障,会为指令的执行提供如下保障
  1. 确保指令重排不会将其后面的代码排到内存屏障之前
  2. 确保指令重排不会将前面的代码排到屏障之后
  3. 确保执行时,屏障前面的代码都执行完毕
  4. 强制将线程工作内存中的值修改刷新到主内存
  5. 如果是写操作,则会导致其他线程工作内存(CPU Cache)中的缓存数据失效

3.3 volatile使用场景

  • 开关控制利用可见性特点
while (!isShutdown && ! isInterrupted()) {
                try {
                    timeUnit.sleep(keepAliveTime);
                } catch (InterruptedException e) {
                    shutdown();
                    break;
                }
       }
  • 状态标记利用顺序性特点
public static Context orderliness() throws NamingException {
        /**有序性:处理器为了提高效率会进行指令重排序 , 单线程不受影响,多线程并发时可能出现问题**/
        if (!initialized) {
            /**有序性
             * 以下进行指令重排,可能导致context为null ,先执行了initialized = true
             * 这时其它线程发现initialized为true而不去初始化context,导致context为null
             */
            context = new InitialContext();
            /**使用volatile修饰,保证在initialized = true时 上面代码已经执行完毕。
             * 这里必须对initialized加volatile,同时保证可见性,防止重复初始化
             * 对context加volatile可以保证content初始化在initialized=true之前执行,
             * 但是无法保证content初始化原子性,而且无法保证initialized对其它线程可见
             * */
            initialized = true;
        }
        return context;
    }
  • 单例Singleton的double-check
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值