描述
-
描述
- volatile是jvm提供的一种轻量级的同步机制(Synchronized是重量级),
-
作用
- 用来确保将变量的更新操作通知到其他线程。
-
为什么是轻量级
- 在访问volatile变量时不会执行加锁操作,因此也不会执行线程阻塞操作。
-
volatile的读性能消耗与普通变量相同,但写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不会发生乱序执行。
特性
- 保证了可见性
- 不保证原子性
- 禁止指令重排
可见性
不用volatile关键字:
代码:
public class JMMDemo {
private static int num = 0;
public static void main(String[] args) {
new Thread(()->{
while (num ==0){
}
}).start();
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num = 1;
System.out.println(num);
}
}
运行结果:并没有结束,而是不断运行循环
解析:
-
开辟一个子线程,只要子线程检测到静态变量为0,就让它一直执行下去。
-
在让num值变为1之前,主线程睡了1s。主线程醒了之后,num的值变成1.但是子线程却却不会停止运行,那么这是什么原因呢?
-
主线程和子线程之间是独立运行的。假如让主线程睡眠1s,子线程无法感知到主线程做了什么。它们之间的连接在这1s中断开了。当主线程睡醒后,和子线程分离了。所以此时子线程还是拿着主线程sleep之前的数据进行操作,而没有进行最新的操作。
怎么解决?
- 当主线程的值出现了覆盖现象时,子线程却无法获取到主线程的最新数据,多线程就会乱套了。
- 所以当多线程操作时,一定要加上同步操作,以确保子线程可以实时更新到新数据。这时候就使用了volatile关键字!
使用volatile关键字:
代码:
运行结果:感知到主线程更新数据,跳出子线程循环
不保证原子性
代码:如果保证了原子性,线程数结果应该是20000
public class Atomic {
private volatile static int num = 0;
public static void add(){
num++;
}
public static void main(String[] args) {
//理论上num结果应该是20x1000=2万
for (int i = 1; i <=20 ; i++) {
new Thread(()->{
for (int j=0; j < 1000 ; j++) {
add();
}
}).start();
}
while (Thread.activeCount()>2) { //如果存活的线程大于2个,即除了main和gc还有其他线程
Thread.yield(); //执行线程礼让
}
System.out.println(Thread.currentThread().getName()+" " +num);
}
}
运行结果:
- main执行的线程每次都不到20000.说明出现了覆盖写的现象,也就是说没有保证原子性;
解析:
num++这个操作分成了3步执行
- 获得这个值
- +1
- 写回这个值
假设线程1执行num++,执行到了第二步+1时还没写入主存时,时间片就用尽了。当其他线程更新完主存后,线程1由于已经执行完获取值的操作而不会再次读取新num值,导致线程1的num值覆盖掉了线程2的num值。故,破坏了原子性。那么怎么来解决这个问题呢?
解决:
- JUC提供了一个atomic包,使用其中的AtomicInteger类,调用num的getAndIncrement方法执行+1操作。这个方法基于CAS原理,它其实是调用操作系统的方法直接修改内存中的值。
- 再次运行时,就保证了原子性,运行结果为200000.
CAS原理
禁止指令重排
描述:
- 计算机并不是按照你写程序的顺序去执行的。
- 指令重排时不同变量如果有依赖关系是不会进行重排的。也就是说,处理器在进行指令重排时会考虑:数据之间的依赖性
volatile是怎么禁止指令重排呢?
- volatile关键字会让这个变量前后都加一层内存屏障,内存屏障的意思是禁止上面指令和下面指令顺序进行交换
场景
哪些地方用到了volatile呢?
- 单例模式
- 读写锁
- JUC中的大量方法