Volatile
- volatile是JAVA虚拟机提供的轻量级同步机制
- 保证可见性
- 不保证原子性
- 禁止指令重排
- JMM(JAVA内存模型)
JMM(Java内存模型 Java Memory Model)本身是一种抽象的概念
,它描述的是一组规则或规范,通过这个规范定义了程序中各个变量(包括实例字段、静态字段和构成数组对象的元素)的访问方式。
JMM关于同步的规定:
- 线程解锁前,必须把共享变量的值刷新会主内存
- 线程加锁前,必须读取主内存最新值到自己的工作内存
- 加锁解锁是同一把锁
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间)。工作内存是每个线程私有数据区域,而JAVA内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作必须在工作内存中进行,首先讲变量从主内存拷贝到自己的工作内存空间,让后对变量进行操作,操作完成后再将变量写回主内存,
不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下:
3. 可见性代码演示
class MyData{
int number = 0;
public void addTo60(){
this.number=60;
}
}
/**
* 1. 验证volatile的可见性
* 1.1 假如int number = 0; number之前没有添加volatile关键字,没有可见性
*/
public class VolatileDemo {
public static void main(String[] args) {
MyData myData = new MyData();//资源类
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t come in");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.addTo60();
System.out.println(Thread.currentThread().getName()
+"\t update number value:"+myData.number);
},"AAA").start();
// 第二个线程就是我们的main线程
while (myData.number==0){
//main线程就在这里一直等待知道number!=0
}
System.out.println(Thread.currentThread().getName()
+"\t mission is over,main get number value:"+myData.number);
}
}
class MyData{
volatile int number = 0;
public void addTo60(){
this.number=60;
}
}
/**
* 1. 验证volatile的可见性
* 1.1 假如int number = 0; number之前没有添加volatile关键字,没有可见性
*/
public class VolatileDemo {
public static void main(String[] args) {
MyData myData = new MyData();//资源类
new Thread(()->{
System.out.println(Thread.currentThread().getName()+"\t come in");
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
myData.addTo60();
System.out.println(Thread.currentThread().getName()
+"\t update number value:"+myData.number);
},"AAA").start();
// 第二个线程就是我们的main线程
while (myData.number==0){
//main线程就在这里一直等待知道number!=0
}
System.out.println(Thread.currentThread().getName()
+"\t mission is over,main get number value:"+myData.number);
}
}
可以发现在没有添加volatile关键字以前,即使AAA线程已经将number的值修改为60,主线程依旧没有跳出while循环,这是因为在主线程的工作内存中依旧是从主内存中读取的number=0;在添加了volatile关键字之后,当AAA线程修改number的值并写回主内存的时候,会通知其他线程number的值已经被修改了,这时候主线程就能够知道number的值已经被修改为60了。
4. 原子性代码演示
class MyData {
volatile int number = 0;
public void addPlusPlus() {
this.number++;
}
}
/**
* 1. 验证volatile的可见性
* 1.1 假如int number = 0; number之前没有添加volatile关键字,没有可见性
* 1.2 添加了volatile 可以保证可见性
* 2. 验证volatile不保证原子性
* 2.1 什么是原子性
* 不可分割,完整性,即某个线程在执行时不可被打断
* 2.2 解决方案
* synchronized、atomicInteger
*/
public class VolatileDemo {
public static void main(String[] args) {
MyData myData = new MyData();
for (int i = 1; i <= 20; i++) {
new Thread(() -> {
for (int i1 = 0; i1 < 1000; i1++) {
myData.addPlusPlus();
}
}, String.valueOf(i)).start();
}
// 等待上边的计算完成后,有main线程取得最终结果
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()
+"\t myData.number = " + myData.number);
}
}
计算得到的结果并没有达到期望值,这是因为在运行过程中,一个线程在还没计算完成的时候,另一个线程抢先计算,也就是说同一个值计算了两次或以上,造成计算结果的不准确。在number++的底层代码中是被拆分成了3个指令:
- 执行getfield拿到原始number
- 执行iadd进行加1操作
- 执行putfield写把累加的值写回主内存
解决方案: - 在方法上添加synchronized关键字
public synchronized void addPlusPlus() {
this.number++;
}
- 使用AtomicInteger
class MyData {
volatile int number = 0;
public void addTo60() {
this.number = 60;
}
public void addPlusPlus() {
this.number++;
}
AtomicInteger atomicInteger = new AtomicInteger();
public void addAtomic(){
atomicInteger.getAndIncrement();
}
}
/**
* 1. 验证volatile的可见性
* 1.1 假如int number = 0; number之前没有添加volatile关键字,没有可见性
* 1.2 添加了volatile 可以保证可见性
* 2. 验证volatile不保证原子性
* 2.1 什么是原子性
* 不可分割,完整性,即某个线程在执行时不可被打断
* 2.2 解决方案
* synchronized、atomicInteger
*/
public class VolatileDemo {
public static void main(String[] args) {
MyData myData = new MyData();
for (int i = 1; i <= 20; i++) {
new Thread(() -> {
for (int i1 = 0; i1 < 1000; i1++) {
myData.addPlusPlus();
myData.addAtomic();
}
}, String.valueOf(i)).start();
}
// 等待上边的计算完成后,有main线程取得最终结果
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()
+"\t myData.number = " + myData.number);
System.out.println(Thread.currentThread().getName()
+"\t myData.atomicInteger = " + myData.atomicInteger.get());
}
}
5. 有序性
计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排,一般分为以下3种:
单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致
处理器在进行重排序时必须要考虑指令之间的数据依赖性
多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性的是无法确定的,结果无法预测
public class MySort {
public static void main(String[] args) {
int x = 11;//语句1
int y = 12;//语句2
x = x + 5;//语句3
y = x * 5;//语句4
/**
* 经过指令重排后的执行顺序可能为:
* 1234
* 2134
* 1324
* 但是语句4即使重排也不会变成第一条,因为违背了指令间的依赖性
*/
}
}
volatile实现禁止指令重排优化
,从而避免多线程环境下程序出现乱序执行的现象
内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,他的左右有两个:
- 保证特定操作的执行顺序
- 保证某些变量的内存可见性
由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和纸条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化
。内存屏障另外一个作用是强制刷出CPU的各种缓存数据,因此热和CPU上的线程都能读取到这些数据的最新版本。