目录:
什么是volatile?
JMM内存模型之可见性
volatile三大特性之一:保证可见性
volatile三大特性之二:不保证原子性
volatile三大特性之三: 禁止指令重排
小结
1. 什么是volatile?
答:volatile是java虚拟机提供的轻量级的同步机制(可以理解成乞丐版的synchronized)
特性有:
保证可见性
不保证原子性
禁止指令重排
理解volatile特性之一保证可见性之前要先理解什么是JMM内存模型的可见性
2. JMM内存模型之可见性
JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。
JMM关于同步的规定:
线程解锁前,必须把共享变量的值刷新回主内存
线程加锁前,必须读取主内存的最新值到自己的工作内存
加锁解锁是同一把锁
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量 的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成 后再将变量写回主内存,不能直接操作主内存中的变量,各个线程中的工作内存中存储着主内存中的变量副本拷贝,因此不同的线
程间无法去访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图:
4.图解:
即创建student对象age=25,每个线程自己的工作内存都会拷贝一份age = 25,当线程t1修改age=37后,需要把age=37写回主内存,然后主内存向其他线程分发最新的值。
volatile保证可见性特性也是如此
volatile三大特性之一:保证可见性
1. 结合代码理解volatile的可见性
代码
import java.util.concurrent.TimeUnit;
class MyData{
int number = 0;
public void addTo60(){
this.number = 60;
}
}
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();
//第2个线程就是我们的main线程
//如果number==0,那么一直在死循环,下面的输出打印不出来,
//如果打印了,就是main线程感知到了number已经从0变为了60,可见性被触发
while (myData.number==0){
// main线程就一直在这里等待循环,直到number值不再为零。
}
System.out.println(Thread.currentThread().getName()+"\t mission is over");
}
}
编译结果:
AAA线程已经把myData.number从0赋值为60,并且写回了主内存,但是对main线程不可见。所以main线程一直在傻傻的等while(myData.number==0),但实际真实值number=60了,
2. 当我们在number添加volatile修饰符,即volatile int number = 0;
代码:
import java.util.concurrent.TimeUnit;
class MyData{
//volatile 增强了主内存和各线程之间的可见性,只有有一个线程改了主内存的值,
// 其他线程马上会收到通知。迅速获得最新值。
volatile int number = 0;
public void addTo60(){
this.number = 60;
}
}
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();
//第2个线程就是我们的main线程
//如果number==0,那么一直在死循环,下面的输出打印不出来,
//如果打印了,就是main线程感知到了number已经从0变为了60,可见性被触发
while (myData.number==0){
// main线程就一直在这里等待循环,直到number值不再为零。
}
System.out.println(Thread.currentThread().getName()+"\t mission is over" +
", main get number value:"+myData.number);
}
}
编译结果:
4. volatile三大特性之二:不保证原子性
1.首先要知道原子性指的是什么意思?
不可分割,完整性,即某个线程正在做某个具体业务时,中间不可以被加塞或者被分割。
需要整体完整。要么同时成功,要么同时失败。
2.通过代码验证volatile不保证原子性
代码:
class MyData {
//volatile 增强了主内存和各线程之间的可见性,只有有一个线程改了主内存的值,
// 其他线程马上会收到通知。迅速获得最新值。
volatile int number = 0;
public void addTo60() {
this.number = 60;
}
//请注意,此时number前面是加了volatile关键字修饰的,volatile不保证原子性。
public void addPlusPlus() {
number++;
}
}
public class VolatileDemo {
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();
}
//需要等待上面20个线程全部计算完成之后,再用main线程取得最终的结果值看是多少
// >2 是因为后台有两个线程,1是main线程,2是GC线程。
// 能最好的控制时间
while (Thread.activeCount()>2){
Thread.yield(); //礼让线程,退不执行。
}
System.out.println(Thread.currentThread().getName()+"\t finally number value:"+myData.number);
}
}
编译结果:
3.如何解决volatile不保证原子性的问题?
我们可以用java.util.concurrent.atomic包下的 AtomicInteger解决这个问题
具体使用如下:
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
class MyData { //MyData.java ==> MyData.class ==> JVM字节码
//volatile 增强了主内存和各线程之间的可见性,只有有一个线程改了主内存的值,
// 其他线程马上会收到通知。迅速获得最新值。
volatile int number = 0;
public void addTo60() {
this.number = 60;
}
//请注意,此时number前面是加了volatile关键字修饰的,volatile不保证原子性。
public void addPlusPlus() {
number++;
}
AtomicInteger atomicInteger = new AtomicInteger();
public void addMyAtomic(){
atomicInteger.getAndIncrement();
}
}
public class VolatileDemo {
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();
}
//需要等待上面20个线程全部计算完成之后,再用main线程取得最终的结果值看是多少
// >2 是因为后台有两个线程,1是main线程,2是GC线程。
// 能最好的控制时间
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);
}
}
编译结果:
4.关于volatile数字丢失的简单原理:
上图解释:
比如拿回自己工作空间的时候都是3,+1后写回去的时候,正好被别的线程捷足先登,只能挂起,已经有线程把4写了回去,等再唤醒的时候再把4写回去就会造成丢值
5. number++在多线程下是不安全的,为什么不用synchronized?
因为synchronized是重锁,有更合适的就用更合适的,杀鸡焉用牛刀。
5. volatile三大特性之三: 禁止指令重排
1. 说指令重排之前,我们要知道什么是有序性?
可能会出现问题,如下:
2. volatile特性之三:禁止指令重排
volatile实现禁止指令重排优化,从而避免多线程环境下程序出现乱序执行的现象。我们先了解一个概念:内存屏障
3.内存屏障
内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,它的作用有两个:
保证特定操作的执行顺序,
保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。
由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能
和这条MemoryBarrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。内存屏障另外一个作
用是强制刷出各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。
6. 小结
工作内存与主内存同步延迟现象导致的可见性问题
解:可以使用Synchronized或volatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见。
对于指令重排导致的可见性问题和有序性问题
解:可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化。