【volatile】volatile关键字的认识


在这里插入图片描述

volatile关键字

只能修饰:类变量和实例变量。
不能修饰:方法参数,局部变量,实例常量,类常量。

机器硬件CPU

CPU Cache模型

CPU Cache由许多Cache Line构造成;
Cache Line为CPU Cache中最小缓存单位。
Cache Line为64字节。

原因

主内存的读写速度慢于CPU Cache的速度

解决

CPU Cache的出现是为了解决CPU直接访问内存效率低下问题的。

流程

程序在运行过程中,将运算所需要的数据从主内存复制一份到CPU Cache中;CPU进行计算时,可以直接对CPU Cache中的数据进行读取和写入。
当运算结束之后,再将CPU Cache中的数据刷新到主内存中。
CPU通过直接访问CPU Cache的方式代替直接访问主内存的方式,大大提高了系统的吞吐量。

CPU缓存一致性问题

主内存与CPU Cache数据一致性。

问题复现:i++操作

读取主内存的i到CPU Cache中;对i进行+1操作;将结果写回CPU Cache中;将CPU Cache中的数据刷新到主内存。
单线程情况下不会出现问题。
多线程情况下操作:假如i原始值为0,由于每个线程都有自己的工作内存(本地内存);两个线程同时进行i++操作,再将本地内存的数据刷新到主内存;
i在进行两次自增之后,还会是1的情况。

解决

方案一:总线加锁。
悲观的实现方式。
会阻塞其他CPU对其他组件的访问;只有一个CPU(抢到总线锁)能够访问这个变量的内存。
方案二:缓存一致性协议。
保证每一个缓存中使用的共享变量副本都是一致的。
当CPU操作Cache中的数据,并且发现该变量为一个共享变量:
读取操作,不作任何处理,只是将Cache中的数据读取到寄存器;
写入操作,发出信号通知其他CPU将Cache Line置为无效状态;其他CPU在进行该变量读取操作时,就会去主内存中获取。

java内存模型

又称JMM(Java Memory Mode)。
java内存模型决定了一个线程对共享变量的写入何时对其他线程可见。
java内存模型定义了线程和主内存之间的抽象关系:
共享变量存在于主内存中,每个线程都可以访问;
每个线程都有自己私有的工作内存又称本地内存;
工作内存只能存储共享变量的副本;
线程不能直接操作主内存,先操作工作内存再写入主内存。

并发编程的三个重要特性

原子性、有序性、可见性。

原子性:
一次操作或者多次操作,要么所有操作都得到了执行并且不会受到任何因素的干扰中断;要么所有的操作都不执行。
volatile不保证原子性,synchronized保证原子性。

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

有序性:
程序代码在执行过程中的先后顺序。

指令重排序:
处理器为了提高程序的运行效率,可能会对输入的代码指令做一定的优化。

JMM如何保证三大特性

JMM可以保证可见性、有序性;不能保证原子性。

原子性:

(1)多个原子性的操作在一起就不再是原子性操作了;
(y++ 步骤一:将y的值写入工作内存;步骤二:在工作内存进行+1操作;步骤三:将y值写入主内存)
(2)简单的读取和赋值操作是原子性的,将一个变量赋值给另一个变量的操作不是原子性的;
(y=x 步骤一:将x从主内存存入到工作内存;步骤二:在工作内存修改y的值为x,再刷新到主内存)
(两个步骤都是原子性的,但是合在一起就不是原子性的)
(3)JMM只保证了基本的读取和赋值的原子性操作。如果想要某些代码片段具有原子性,需要使用关键字synchronized,或者JUC中的lock。

可见性

(1)volatile:
能够保证可见性。当一个变量被volatile关键字修饰时,对于共享变量的读操作,会直接在主内存中进行;对于共享变量的写操作时,会先修改工作内存,修改后立刻刷新到主内存。
(2)synchronized:
能够保证可见性。保证同一时刻只有一个线程获得锁,然后执行同步方法;并且确保在释放锁之前,会将对变量的修改刷新到主内存中。
(3)JUC的显示锁lock:
能够保证可见性。Lock的lock方法能够保证在同一时刻只有一个线程获得锁,然后执行同步方法;并且确保在释放锁之前,会将对变量的修改刷新到主内存中。

有序性

happens-before原则:
(1)程序次序规则:
在一个线程内,代码按照编写时的次序执行,编写在后面的操作发生于编写在前面的操作之后。
(只要确保在一个线程内最终的结果和代码顺序执行的结果一致)
(2)锁定规则:
一个unlock操作要先行发生于对同一个锁的lock操作。
(无论单线程还是多线程环境下,同一个锁是锁定状态,必须先对其执行释放操作之后才能继续进行lock操作)
(3)volatile变量规则:
对一个变量的写操作要早于对这个变量之后的读操作。
(如果一个变量使用volatile关键字修饰,一个线程对他进行读操作,一个线程对他进行写操作;那么写入操作肯定要先行发生于读操作)
(4)传递规则:
如果操作A先于操作B,操作B先于操作C,那么操作A一定先于操作C。说明happens-before具备传递性。
(5)线程启动规则:
Thread对象的start()方法先行发生于对该线程的任何动作。只有start之后线程才算真正运行,否则Thread也只是一个对象而已。
(6)线程中断规则:
对线程执行interrupt()方法肯定要优先于捕获到中断信号。
(7)线程的终结规则:
线程中的所有操作都要先行发生于线程的终止检测。
(8)对象的总结规则:
一个对象的初始化的完成先行发生于finalize()方法之前。

volatile关键字的语义

(1)保证了不同线程之间对共享变量操作时的可见性。(当一个线程修改被volatile修饰的变量,另外一个线程会立即看到最新的值)
(2)禁止对指令进行重排序操作。

volatile的原理和实现机制

可见性和有序性的实现。

可见性

当一个线程修改被volatile修饰的变量,另外一个线程会立即看到最新的值。

有序性

被volatile修饰的变量存在于一个"lock;"的前缀。
前缀相当于一个内存屏障。
(1)确保指令重排序时不会将其后面的代码排到内存屏障之前。
(2)确保指令重排序时不会将其前面的代码排到内存屏障之后。
(3)确保在执行到内存屏障修饰的指令时前面的代码全部执行完成。
(4)强制将线程工作内存中值的修改刷新到主内存中。
(5)如果是写操作,则会导致其他线程工作内存(CPU Cache)中的缓存数据失效。

volatile使用的场景

(1)开关控制(利用可见性特点);
(2)状态标记(利用顺序性特点);
(3)单例设计模式的double-check(利用了顺序性特点)。

volatile和synchronized

  1. 使用上的区别:

volatile:
修饰类变量和实例变量。
修饰的变量可以为null。

synchronized:
修饰方法或者语句块。
同步语句块的monitor对象不能为null。

  1. 对原子性的保证:

volatile:
无法保证原子性。

synchronized:
是一种排他机制;修饰的同步代码块不能被中途打断;可以保证代码的原子性。

  1. 对可见性的保证:

volatile:
使用机器指令"lock;"的方式迫使其他线程工作内存中的数据失效,不得不到主内存中进行再次加载。

synchronized:
借助JVM指令monitor enter和monitor exit对通过排他的方式使得同步代码块串行化,在monitor exit时所有共享资源都将刷新到主内存。

  1. 对有序性的保证:

volatile:
禁止JVM编码器以及处理器对其进行重排序。

synchronized:
可以发生指令重排,但是最终输出结果和代码编写顺序输出结果一致。

  1. 其他:

volatile不会使线程陷入阻塞。

synchronized会使线程进入阻塞状态。

【读于汪文君老师的书籍】

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值