Volatile是Java虚拟机提供轻量级的同步机制
- 保证可见性
- 不保证原子性
- 禁止指令重排
可见性
可见性与Java的内存模型(JMM)有关,模型采用缓存与主存的方式对变量进行操作,也就是说,每个线程都有自己的缓存空间,对变量的操作都是在缓存中进行的,之后再将修改后的值返回到主存中,这就带来了问题,有可能一个线程在将共享变量修改后,还没有来的及将缓存中的变量返回给主存中,另外一个线程就对共享变量进行修改,那么这个线程拿到的值是主存中未被修改的值,这就是可见性的问题。
volatile很好的保证了变量的可见性,变量经过volatile修饰后,对此变量进行写操作时,汇编指令中会有一个LOCK前缀指令,这个不需要过多了解,但是加了这个指令后,会引发两件事情:
- 将当前处理器缓存行的数据写回到系统内存
- 这个写回内存的操作会使得在其他处理器缓存了该内存地址无效
意思就是说当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值,这就保证了可见性。
eg://没有使用volatile,Thread1线程对主内存中的变价不知道,加了volatile可以保证可见性
package lin.jmm;
import java.util.concurrent.TimeUnit;
public class Test01 {
private static int num=0;
public static void main(String[] args) {
new Thread(()->{
while (num==0){
}
},"Thread1").start();
//停一秒,使Thread1线程启动
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num=1;
System.out.println(num);
}
}
eg://使用volatile
package lin.jmm;
import java.util.concurrent.TimeUnit;
public class Test01 {
private volatile static int num=0;
public static void main(String[] args) {
new Thread(()->{
while (num==0){
}
},"Thread1").start();
//停一秒,使Thread1线程启动
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
num=1;
System.out.println(num);
}
}
原子性(不可分割)
首先需要了解的是,Java中只有对基本类型变量的赋值和读取是原子操作,如i = 1的赋值操作,但是像j = i或者i++这样的操作都不是原子操作,因为他们都进行了多次原子操作,比如先读取i的值,再将i的值赋值给j,两个原子操作加起来就不是原子操作了。
线程A在执行任务的时候,不能被打扰,不能被分割,要么同时成功,要么同时失败。
所以,如果一个变量被volatile修饰了,那么肯定可以保证每次读取这个变量值的时候得到的值是最新的,但是一旦需要对变量进行自增这样的非原子操作,就不会保证这个变量的原子性了。
package lin.volataile_Test;
//不保证原子性
public class Test02 {
private volatile static int sum=0;
public static void add(){
sum++;
}
public static void main(String[] args) {
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) { //正常sum为2万
add();
}
}).start();
}
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+" "+sum);
}
}
加了synchronized 或 lock锁,就可以保证原子性
如果不使用 lock 和 synchronized,怎么保证原子性?
使用原子类,解决原子性问题
package lin.volataile_Test;
import java.util.concurrent.atomic.AtomicInteger;
//不保证原子性
public class Test02_AtomicInteger {
private volatile static AtomicInteger sum = new AtomicInteger();
public static void add(){
//sum++; 因为是一个类,所以不能直接++操作
sum.getAndIncrement(); //AtomicInteger 的+1方法: CAS
}
public static void main(String[] args) {
for (int i = 1; i <= 20; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) { //正常sum为2万
add();
}
}).start();
}
while (Thread.activeCount()>2){
Thread.yield();
}
System.out.println(Thread.currentThread().getName()+" "+sum);
}
}
这些类的底层都是直接和操作系统挂钩!在内存中修改值!Unsafe类是一个很特殊的存在!
指令重排
什么是指令重排?
源代码-->编译器优化的重排-->指令并行也可能会重排-->内存系统也会重排-->执行
处理器在进行指令重排的时候,会考虑数据之间的依赖性!
volatile可以避免指令重排问题:
内存屏障。CPU指令。作用:
1、保持特定的操作的执行顺序!
2、可以保证某些变量的内存可见性(利用这些特性volatile实现了可见性)
只要加了volatile就会在上面跟下面加一个内存屏障