volatile是什么
volatile是JVM提供的轻量级的同步机制
保证可见性
不保证原子性
禁止指令重排(保证有序性)
JMM内存模型之可见性
JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
JMM关于同步的规定:
线程解锁前,必须把共享变量的值刷新回主内存
线程加锁前,必须读取主内存的最新值到自己的工作内存
加锁解锁是同一把锁
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:
可见性
通过前面对JMM的介绍,我们知道各个线程对主内存中共享变量的操作都是各个线程各自拷贝到自己的工作内存进行操作后再写回到主内存中的。
这就可能存在一个线程AAA修改了共享变量X的值但还未写回主内存时,另外一个线程BBB又对主内存中同一个共享变量X进行操作,但此时A线程工作内存中共享变量x对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题
/**
* 测试可见性
*/
class MyData{
int number = 0;
public void addT030(){
this.number += 30;
}
}
public class VolatileVisibility {
public static void main(String[] args) {
// 共享资源
MyData data = new MyData();
// 线程处理业务,3秒后修改共享变量
new Thread(()->{
System.out.println(Thread.currentThread().getName()+ " come in");
try {
TimeUnit.SECONDS.sleep(3);
data.addT030();
System.out.println(Thread.currentThread().getName()+ " updated");
} catch (InterruptedException e) {
e.printStackTrace();
}
},"AAA").start();
// 主线程 如果发现共享变量改变了,立即退出
while (data.number == 0){}
System.out.println(Thread.currentThread().getName()+ " mission success");
}
}
结果
即使线程AAA修改了变量写回了内存,但是主线程工作空间中,变量值一直是0 无法退出,因为线程间变量的修改是不可见的,没有通知主线程,内存中的共享变量发生了改变。
因此,需要有一种机制,能够是线程间能够相互看到共享变量。
volatile保证可见性
/**
* 测试可见性
*/
class MyData{
volatile int number = 0;
public void addT030(){
this.number += 30;
}
}
使用volatile。轻量级锁。使得共享资源可以相互可见。
volatile不保证原子性
原子性指的是什么意思?
不可分割,完整性,也即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割。需要整体完整要么同时成功,要么同时失败。
volatile不保证原子性案例演示:
/**
* 测试可见性
*/
class MyData{
volatile int number = 0;
public void addT030(){
this.number += 30;
}
public void increase(){
this.number++;
}
}
public class VolatileTest {
public static void main(String[] args) {
MyData data = new MyData();
// 启动20个线程 对同一变量操作
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
data.increase();
}
},String.valueOf(i)).start();
}
// 当活跃的线程数量 大于2时,主线程进入就绪状态,等待cpu执行
// 默认两个线程是 main,gc垃圾回收 两个线程。只有只剩两个线程时,主线程继续往下执行
while (Thread.activeCount()>2){
Thread.yield();
}
// 按理说值是20000 但是大部分情况下值不是20000
System.out.println("value "+data.number);
}
}
volatile不保证原子性理论解释
number++在多线程下是非线程安全的。
我们可以将代码编译成字节码,可看出number++被编译成3条指令。
假设我们没有加 synchronized那么第一步就可能存在着,三个线程同时通过getfield命令,拿到主存中的 n值,然后三个线程,各自在自己的工作内存中进行加1操作,但他们并发进行 iadd 命令的时候,因为只能一个进行写,所以其它操作会被挂起,假设1线程,先进行了写操作,在写完后,volatile的可见性,应该需要告诉其它两个线程,主内存的值已经被修改了,但是因为太快了,其它两个线程,陆续执行 iadd命令,进行写入操作,这就造成了其他线程没有接受到主内存n的改变,从而覆盖了原来的值,出现写丢失,这样也就让最终的结果少于20000。
volatile不保证原子性问题解决(Atomic原子类),多线程环境下i++等问题解决
可加synchronized解决,但它是重量级同步机制,性能上有所顾虑。
如何不加synchronized解决number++在多线程下是非线程安全的问题?使用AtomicInteger。
class MyData{
volatile int number = 0;
AtomicInteger atomicInteger = new AtomicInteger();
public void addAtomic(){
atomicInteger.getAndIncrement();
}
public void addT030(){
this.number += 30;
}
public void increase(){
this.number++;
}
}
public class VolatileTest {
public static void main(String[] args) {
MyData data = new MyData();
// 启动20个线程 对同一变量操作
for (int i = 0; i < 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
data.increase();
data.addAtomic();
}
},String.valueOf(i)).start();
}
// 当活跃的线程数量 大于2时,主线程进入就绪状态,等待cpu执行
// 默认两个线程是 main,gc垃圾回收 两个线程。只有只剩两个线程时,主线程继续往下执行
while (Thread.activeCount()>2){
Thread.yield();
}
// 按理说值是20000 但是大部分情况下值不是20000
System.out.println(" volatile value "+data.number);
System.out.println(" atomicInteger value "+data.atomicInteger);
}
}
结果:
volatile指令重排案例1
计算机在执行程序时,为了提高性能,编译器和处理器的常常会对指令做重排,一般分以下3种:
单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。
处理器在进行重排序时必须要考虑指令之间的数据依赖性
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
重排案例1
public void mySort{
int x = 11;//语句1
int y = 12;//语句2
× = × + 5;//语句3
y = x * x;//语句4
}
可重排序列:
1234
2134
1324
问题:请问语句4可以重排后变成第一个条吗?答:不能。
重排案例2
int a,b,x,y = 0
这也就说明在多线程环境下,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的。
volatile指令重排案例2
以下程序在高并发的情况下,执行结果不一样
public class ReSortSeqDemo{
int a = 0;
boolean flag = false;
public void method01(){
a = 1;//语句1
flag = true;//语句2
}
public void method02(){
if(flag){
a = a + 5; //语句3
System.out.println("retValue: " + a);//可能是6或5
}
}
}
多线程环境中线程交替执行method01()和method02(),由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测。
禁止指令重排小总结
volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象
先了解一个概念,内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:
- 保证特定操作的执行顺序,
- 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。
由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
对volatile变量进行写操作时,会在写操作后加入一条store屏障指令,将工作内存中的共享变量值刷新回到主内存。
对Volatile变量进行读操作时,会在读操作前加入一条load屏障指令,从主内存中读取共享变量。
线性安全性获得保证
工作内存与主内存同步延迟现象导致的可见性问题 - 可以使用synchronized或volatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。
对于指令重排导致的可见性问题和有序性问题 - 可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化。
单例模式也会出现并发安全问题
public class SingletonTest {
private static SingletonTest instance = null;
private SingletonTest(){
System.out.println(Thread.currentThread().getName()+ "构造方法被执行");
}
public static SingletonTest getInstance(){
if(instance == null){
instance = new SingletonTest();
}
return instance;
}
}
多线程下,单例模式出现了并发安全问题
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
SingletonTest.getInstance();
},String.valueOf(i)).start();
}
}
执行结果
按理说,单例模式,应该只出现一个实例,但实际上出现了多个(多线程下)
解决方法:
方法一:使用synchronized
public class SingletonTest {
private static SingletonTest instance = null;
private SingletonTest(){
System.out.println(Thread.currentThread().getName()+ "构造方法被执行");
}
public static SingletonTest getInstance(){
// dcl 双端检锁机制
// 内层检查控制并发情况下,单实例问题
// 外层检查控制并发情况下,效率问题
if(instance == null){
synchronized (SingletonTest.class){
if(instance == null){
instance = new SingletonTest();
}
}
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
SingletonTest.getInstance();
},String.valueOf(i)).start();
}
}
}
注意:这里采用了dcl双端检锁机制,保证并发的同时,保证效率
方法二: synchronized+ volatile
为什么要用volatile
我们看看 创建对象的过程,在并发环境下,是否会有问题
instance = new SingletonTest();可以分为以下3步完成(伪代码):
memory = allocate(); //1.分配对象内存空间
instance(memory); //2.初始化对象
instance = memory; //3.设置instance指向刚分配的内存地址,此
步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单线程中并没有改变,因此这种重排优化是允许的。
memory = allocate(); //1.分配对象内存空间
instance = memory;//3.设置instance指向刚分配的内存地址,此时
instance(memory);//2.初始化对象
但是指令重排只会保证串行语义的执行的一致性(单线程),但并不会关心多线程间的语义一致性。
在多线程情况下,可能没有实例化完成, 对象引用就指向了对应地址。对象!=null,但是内部没有数据,调用对应方法时,得不到对应结果。因此我们要禁止指令重排
public class SingletonTest {
private static volatile SingletonTest instance = null;
private SingletonTest(){
System.out.println(Thread.currentThread().getName()+ "构造方法被执行");
}
public static SingletonTest getInstance(){
// dcl 双端检锁机制
// 内层检查控制并发情况下,单实例问题
// 外层检查控制并发情况下,效率问题
if(instance == null){
synchronized (SingletonTest.class){
if(instance == null){
instance = new SingletonTest();
}
}
}
return instance;
}
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(()->{
SingletonTest.getInstance();
},String.valueOf(i)).start();
}
}
}