volatile 是多线程中必学的知识点,可以用该修饰词去保证资源的可见性以及禁止指令重排。
volatile 定义:
volatile 是 java虚拟机提供的轻量级的同步机制。
特性:
1.可见性
2.禁止指令重排
但是不保证原子性。下面会逐一进行讲解
可见性:
首先要了解一个概念,在java内存模型JMM中有说明,变量的值存放在主内存中,线程创建时,jvm会为其分配一个私有的内存工作空间,线程要修改主内存中的变量的值时,
需要先copy一份副本到自己的工作内存中。修改完毕再写回到主内存中。注意:线程之间不能访问其他线程的工作内存。这样就导致,线程1和线程2同时复制了一份变量A的副本到自己的工作内存内,线程1先修改完毕并回写进主内存中,这时线程2还是操作的线程1修改之前的从主内存中复制的数据副本,对线程1的修改并不知情!这就导致了线程2再回写到主内存中时覆盖了线程1的回写数据。那不就乱套了
可见性是指:线程1在自己的工作空间修改变量A并回写到主程序后,线程2会接收到通知,随后会清除本地的内存空间中的A副本并重新从主内存copy 一份最新的变量副本 重新执行逻辑!
代码示例:首先看一下 线程间不可见 的代码以及效果!
public class VolatileDemo {
private int a = 0;
private void addTo20(){
a += 20;
}
public static void main(String[] args) {
VolatileDemo volatileDemo = new VolatileDemo();
new Thread(()->{
try {
//等待一会,等其他线程把变量复制到各自的内存空间
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
volatileDemo.addTo20();
System.out.println(Thread.currentThread().getName() + "\t run over ,a value:" + volatileDemo.a );
},"AAA").start();
while (volatileDemo.a == 0){
//线程等待其他线程修改完毕后
}
System.out.println(Thread.currentThread().getName() + "\t run over,a value : " + volatileDemo.a);
}
}
这里面 main是一个线程,new Thread 又创建了一个线程 AAA 在线程 AAA中调用 addTo20方法 修改 变量a的值为20.在main线程中while等待 变量a值的变化 值改变后会输出最后一条打印消息!
执行结果:main程序一直在循环中,并没有检测到变量a的值已经被其他线程改变了。
下面,我们加上volatile 修饰后,再次执行main方法
private volatile int a = 0;
执行结果:main方法的最后一行成功执行并打印出了a.value = 20
以上代码证明了 volatile的特性1:可见性。
2.禁止指令重排。
首先了解下 指令重排的概念 ,指令重排 是指编辑器和cpu有时会为了优化程序的执行效率,调整程序的执行顺序,指令重排只可能发生在毫无关系的指令之间, 如果指令之间存在依赖关系, 则不会重排.
举例:
int a = 0;//指令1
int b = 0;//指令2
a +=b;//指令3
按照程序的编写顺序 是 1,2,3顺序执行,但是在指令重排后,可能会 2,1,3这样的顺序执行,因为指令1和2毫无关系,先执行谁都可以。指令3由于使用到了指令1和指令2的值 所以只会等待 指令1和2执行完毕后再执行指令3
按理说是做的优化,为什么还要禁止呢?这里我们拿一个单例的创建代码举个例子:
class MyData{
private static MyData myData;
private MyData(){
System.out.println("MyData init");
}
/**
* 获取单例bean的方法
* @return
*/
public static MyData getInstance(){
if(myData == null){
synchronized (MyData.class){
if(myData == null){
myData = new MyData();
}
}
}
return myData;
}
}
上面的代码就是一个获取单例Bean的方法!基本看不出什么问题吧!但是上述代码同样存在着指令重排的风险。虽然几率很小!
原因出在了 new MyData()这一句代码上 这句话写代码是一句话,但是到了底层执行时 会分成多个指令,下面是我自己的理解,可能有偏差,欢迎指正!
1.开辟所需内存空间
2.初始化对象,数据创建到开辟好的内存空间中
3.变量引用指向开辟的内存空间地址
正常来说 1,2,3顺序执行没啥问题,如果指令重排后可能会出现 1,3,2的执行顺序(2和3互不影响,也不存在依赖关系,所以2和3可能会被重排)。单线程下没有什么问题!但是多线程下,
线程1进来判断myData对象不存在 则进去创建对象 执行的顺序是 1,3,2 当执行完3后 myData已经有引用了。虽然指向的内存空间中还未创建数据!
这时另一个线程被分配了时间片执行 if(myData == myData)这句就会返回false,然后return了一个 空内存的对象,后续的一些基于该对象的操作就会出现执行错误!
解决方法:用 volatile修饰变量就可以禁止 指令被重排。如下
private static volatile MyData myData;
第三点:就是volatile不保证原子性。
虽然提供了可见性。但是由于线程执行过快,线程1修改工作内存中的变量值并且写入到主内存中,其他线程在还未接收到通知就执行了写入操作,就会导致线程1的写入被覆盖了。下面代码举例:
public class VolatileDemo {
private volatile int a = 0;
private void addTo20(){
a += 20;
}
private void addAdd(){
a++;
}
public static void main(String[] args) {
VolatileDemo volatileDemo = new VolatileDemo();
for (int i = 0;i<10;i++){
new Thread(()->{
for(int j = 0;j<1000;j++){
volatileDemo.addAdd();
}
}).start();
}
//这里等待线程全部执行完毕,大于2是因为 有一个main线程和一个gc的线程
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println("a value:" + volatileDemo.a);
}
}
启动了10个线程 ,每个线程执行1000次 addAdd 方法 相当于执行了 1000次+1 ,我们期望结果 a = 10000.但实际执行后
会发现 小于10000 ,原因就是有些线程的++后写入到主内存后,其他线程未及时被通知到主内存中的a修改就执行了写入操作。导致之前的线程写入被覆盖了!
解决方法:可以使用 java.util.concurrent.atomic 包下的 AtomicInteger 对象
Atomic 是原子性的意思 加上 Integer 就是 原子性的 Integer。具体的api可参考 javaApi文档
完善后的代码
public class VolatileDemo {
private volatile int a = 0;
private AtomicInteger atomicInteger = new AtomicInteger();
private void addTo20(){
a += 20;
}
private void addAdd(){
a++;
}
private void myIncrementAndGet(){
atomicInteger.incrementAndGet();
}
public static void main(String[] args) {
VolatileDemo volatileDemo = new VolatileDemo();
for (int i = 0;i<10;i++){
new Thread(()->{
for(int j = 0;j<1000;j++){
volatileDemo.addAdd();
volatileDemo.myIncrementAndGet();
}
}).start();
}
//这里等待线程全部执行完毕,大于2是因为 有一个main线程和一个gc的线程
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println("a value:" + volatileDemo.a);
System.out.println("atomicInteger value:" + volatileDemo.atomicInteger);
}
}
增加成员变量 private AtomicInteger atomicInteger = new AtomicInteger(); 增加 atomicInteger 变量的++方法 myIncrementAndGet 在线程中调用 volatileDemo.myIncrementAndGet();每次+1 预期结果为 atomicInteger 的值为10000 执行结果:
完毕!!!!