Volatile学习
JMM(java内存模型)
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中运行,首先要将变量从主内存中拷贝到自己的工作内存,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中变量,各个线程中的工作内存中存储着主内存中的变量的副本拷贝,因此不同的线程间无法访问对方的工作内存,线程间通信(传值)必须通过主内存来实现
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AmvwipSH-1616739889774)(C:\Users\lty\AppData\Roaming\Typora\typora-user-images\image-20210325215907137.png)]
JMM三大特性:
1)可见性 2)原子性 3)有序性
Volatile关键字
Volatile是java虚拟机提供的一种轻量级的同步机制
特性:1)保证可见性 2)不保证原子性 3)禁止指令重排
1)保证了用volatile修饰的变量对所有线程的可见性。
可见性:当一个线程修改了变量的值,新的值会立刻同步到主内存当中。
而其他线程读取这个变量的时候,也会从主内存中拉取最新的变量值。
代码演示:
1)没有使用volatile
public class VolatileTest01 {
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();
while (myData.number == 0) {
//main线程持有共享数据的拷贝,一直为0
}
System.out.println(Thread.currentThread().getName() + "\t mission is over. main get number value: " + myData.number);
}
}
class MyData {
int number = 0;
public void addTo60() {
this.number = 60;
}
}
//结果
AAA come in
AAA update number value: 60
结果:程序一直在运行,主线程main检测到的number一直是0,证明线程AAA虽然改变了number 的值,但是与main线程之间没有实现可见性,主线程的number还是0.
使用volatile之后
package com.lty;
import java.util.concurrent.TimeUnit;
public class VolatileTest01 {
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();
while (myData.number == 0) {
//main线程持有共享数据的拷贝,一直为0
}
System.out.println(Thread.currentThread().getName() + "\t mission is over. main get number value: " + myData.number);
}
}
class MyData {
volatile int number = 0;//使用volatile
//int number = 0;
public void addTo60() {
this.number = 60;
}
}
//结果
AAA come in
AAA update number value: 60
main mission is over. main get number value: 60
结果:mian线程的number值是60,循环结束
2)volatile不保证原子性
什么是原子性?
原子性是指一个操作是不可中断的,要么全部执行成功要么全部执行失败,有着“同生共死”的感觉。及时在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。
案例:
package com.lty;
import java.util.concurrent.TimeUnit;
public class VolatileTest01 {
public static void main(String[] args) {
MyData myData = new MyData();
for (int i = 1; i <= 20; i++) {
new Thread(() -> {
for (int j = 1; j <= 1000; j++) {
myData.addPlusPlus();
}
},String.valueOf(i)).start();
}
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"\t int type finally number value: "+myData.number); //结果并不是:20000
}
//volatile可以保证原子性,及时通知其他线程,主物理内存中值已经被修改过
private static void seeOkByVilatile() {
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();
while (myData.number == 0) {
//main线程持有共享数据的拷贝,一直为0
}
System.out.println(Thread.currentThread().getName() + "\t mission is over. main get number value: " + myData.number);
}
}
class MyData {
//volatile int number = 0;
int number = 0;
public void addTo60() {
this.number = 60;
}
public void addPlusPlus(){
this.number++;
}
}
结果并不是:20000
为什么不保证原子性?
这是因为,比如一条number++的操作,会形成3条指令。
getfield //读
iconst_1 //++常量1
iadd //加操作
putfield //写操作
假设有3个线程,分别执行number++,都先从主内存中拿到最开始的值,number=0,然后三个线程分别进行操作。假设线程0执行完毕,number=1,刚要写回主内存时可能被挂起。但是此时线程1、2已经拿到了number=0,并运行完将结果写回主内存之后,线程0才继续获得时间片,将之前算的结果写入主内存,所以结果就是写覆盖。这样就会造成数据丢失。
解决办法:
1)加synchronized锁。(这种方法不推荐,有杀鸡用牛刀的感觉)
2)使用java.util.concurrent.AtomicInteger
package com.lty;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
public class VolatileTest01 {
public static void main(String[] args) {
MyData myData = new MyData();
for (int i = 1; i <= 20; i++) {
new Thread(() -> {
for (int j = 1; j <= 1000; j++) {
myData.addPlusPlus();
myData.addMyAtomic();
}
},String.valueOf(i)).start();
}
while (Thread.activeCount() > 2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+"\t int type finally number value: "+myData.number);
System.out.println(Thread.currentThread().getName()+"\t AtomicInteger type finally number value: "+myData.atomicInteger);
}
}
class MyData {
//volatile int number = 0;
int number = 0;
public void addTo60() {
this.number = 60;
}
public void addPlusPlus(){
this.number++;
}
AtomicInteger atomicInteger = new AtomicInteger();
public void addMyAtomic(){
atomicInteger.getAndIncrement();//这个方法是使atomicInteger自加1.
}
}
//结果 :可见,由于`volatile`不能保证原子性,出现了线程重复写的问题,最终结果比20000小。而`AtomicInteger`可以保证原子性。
main int type finally number value: 19496
main AtomicInteger type finally number value: 20000
3)volatile的有序性,禁止指令重排
volatile可以保证有序性,也就是防止指令重排序。所谓指令重排序,就是出于优化考虑,CPU执行指令的顺序跟程序员自己编写的顺序不一致。
就好比一份试卷,题号是老师规定的,是程序员规定的,但是考生(CPU)可以先做选择,也可以先做填空。
int x = 11; //语句1
int y = 12; //语句2
x = x + 5; //语句3
y = x * x; //语句4
以上例子,可能出现的执行顺序有1234、2134、1342,这三个都没有问题,最终结果都是x = 16,y=256。但是如果是4开头,就有问题了,y=0。这个时候就不需要指令重排序。
volatile底层是用CPU的内存屏障(Memory Barrier)指令来实现的,有两个作用,一个是保证特定操作的顺序性,二是保证变量的可见性。在指令之间插入一条Memory Barrier指令,告诉编译器和CPU,在Memory Barrier指令之间的指令不能被重排序。