volatile是什么?
volatile是Java虚拟机提供的轻量级的同步机制
- 保证可见性
- 不保证原子性
- 禁止指令重排
JMM(Java内存模型)
JMM本身是一种抽象的概念模型并不真实存在,塔描述的是一组规则或规范,通过这组规范定义了程序中各个变量(实例字段、静态字段、构成数组对象的元素)的访问方式
- 线程解锁前,必须把共享变量的值刷新回主内存
- 线程加锁前,必须读取主内存的最新值到自己的工作内存
- 加锁解锁是同一把锁
由于JVM运行程序的实体是线程,而每个线程创建JVM都会为其创建一个工作内存(栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在 ,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作 (读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的 ,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:
可见性验证
class MyData {
int number;
public void setNumber60() {
number = 60;
}
}
/**
* 验证volatile可见性
*
* 假如int number = 0; 没有添加volatile关键字
*/
public class VisiblenessTest {
public static void main(String[] args) {
// 资源类
MyData myData = new MyData();
// 线程A
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t come in");
// 模拟操作时间 3S
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.setNumber60();
System.out.println(Thread.currentThread().getName() + "\t updated number value " + myData.number);
}, "A").start();
// 线程B
new Thread(() -> {
while (myData.number == 0){}
System.out.println(Thread.currentThread().getName() + "\t is over");
}, "B").start();
}
}
输出:
A come in
A updated number value 60
但程序不结束
解析:
A线程、B线程使用同一份数据
A线程操作后,number已经被改变为60
B线程并不知道number已经被改变,所以一直在循环
使用volatile修饰number后
class MyData {
volatile int number;
public void setNumber60() {
number = 60;
}
}
输出:
A come in
A updated number value 60
B is over
Process finished with exit code 0
B线程输出,程序正常结束
验证了volatile能够保证线程可见性,在A线程操作number后,B线程能够知道A线程修改后的值,结束循环,并正常输出
结论:
volatile可以保证可见性
原子性验证
原子性:不可分割,完整性,即某个线程正在做某个业务是,中间不可以呗分割,需要整体完成,要不同时成功,要不同时失败
class MyData2{
volatile int number;
public void add() {
number++;
}
}
/**
* 验证volatile的原子性
*
* 原子性:不可分割,完整性,即某个线程正在做某个业务是,中间不可以呗分割,需要整体完成,要不同时成功,要不同时失败
*/
public class AtomicityTest {
public static void main(String[] args) {
MyData2 myData = new MyData2();
// 20个线程,每个线程调用1000次,正常清苦,最终number值应该为20000
for (int i = 0; i < 20; i++) {
new Thread(() -> {
for (int j = 0; j < 1000; j++) {
myData.add();
}
}).start();
}
// 等待线程执行结束,获取myData.number,查看结果
// 后台线程:Main GC,抛开他们两个
while(Thread.activeCount() > 2) {}
System.out.println(Thread.currentThread().getName() + "\t get number:" + myData.number);
}
}
输出:
main get number:18576
Process finished with exit code 0
20个线程,每个线程执行1000次,理论情况输出数值应该为20000,实际与理论不符,多次执行,发现输出小于20000
解析:
number++是非线程安全的
number++会被分为三步
1、获取number当前值,记作n1——也就是上面所讲的JMM中将主内存加载到工作内存
2、将n1值加一,记作n2——在工作内存中操作
3、将n2赋值给number——将工作内存中内容重新写入主内存
Compiled from "AtomicityTest.java"
class volatileTest.MyData2 {
volatile int number;
volatileTest.MyData2();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
public void add();
Code:
0: aload_0
1: dup
2: getfield #2 // Field number:I
5: iconst_1
6: iadd
7: putfield #2 // Field number:I
10: return
}
注意看add方法中
getfield #2 获取值
iconst_1 定义整形1
iadd 相加
putfield #2 放回去
在多线程情况下,T1获取number值为0,T2获取number值为0,在T1将number加为1时,T2获取到CPU,T1被挂起,这是主内存中number还是0,T2也将number加为1,并写会主内存,T1继续执行,再写一遍,则正常情况number现在的值应该为2,但是还是1,出现了线程操作丢失的问题,所以输出永远小于20000
那么如何解决原子性问题呢?
1、加锁Synchronized Lock
2、原子类操作 将number设为AtomicInteger
-
有序性
计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排,一般分为以下三种:
单线程环境里面确保程序最终执行结果和代码顺序执行的结果一直
处理器在进行重排序时必须要考虑指令之间的数据依赖性
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测
例:
{
int x = 2; // 语句1
int y = 3; // 语句2
x = x + 5; // 语句3
y = x * x; // 语句4
}
因为Java会先对源码进行编译,那么编译后的代码顺序有可能会与源码不一致
例如语句1和语句2没有先后顺序要求,那么最终的顺序有可能是:
1234、2134、1324
但是4一定不会在前面,因为他要依赖x的值,且过程中x的值有所改变
但是多线程下指令重排可能会导致数据执行不一致的情况,无法保证数据的最终一致性
public class ReSortSeqTest {
int a = 0;
boolean flag = false;
public void method1() {
a = 1; // 语句1
flag = true; // 语句2
}
public void method2() {
if (flag) {
a = a + 5;
System.out.println("****** return:" + a);
}
}
}
如果顺序执行,那么方法2输出应该为6
但是如果在多线程且指令重排的情况下,有可能语句2先于语句1执行,但是执行完语句2,被其他线程抢占了CPU,执行方法2,那么此时a的值仍未默认值0,那么输出5,此时语句1才执行,a又被赋值为1,此时的情况我们无法准确地考虑最终的结果,但是指令重排不好演示,所以只能枯燥的讲一下,后续如果有更好的演示方式,我再进行更新
volatile怎么解决这个问题的?
volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象
首先了解一个概念,内存屏障又称内存栅栏,是一个CPU指令,它的作用有两个
1、保证特定操作的执行顺序
2、保证某些变量的内存可见性
由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作用是强制刷出CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本
内容均来源于学习资料,在学习过程中进行记录,如有侵权联系作者进行删除
Change the world by program
作者:小怪兽说疼疼哒
原文链接:https://www.jianshu.com/p/0f8b1066ca64