1 简介
Volatile保证了可见性和有序性,没有保证原子性。
1.1 保证可见性简介
可见性就是指当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改。volatile变量做到了这一点。
Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是volatile变量都是如此。普通变量与volatile变量的区别是,volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。因此我们可以说volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点
1.2 保证有序性简介
在对Volatile修饰的变量的写操作前面会加一个StoreStore屏障,后面加一个StoreLoad屏障
在对Volatile修饰的变量的读操作后面会加一个LoadLoad屏障和一个LoadStore屏障
这样子保证了有序性
1.3 volatile的内存语义
当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值立即刷新回主内存中。
当读一个volatile变量时,JMM会把该线程对应的本地内存设置为无效,直接从主内存中读取共享变量
所以volatile的写内存语义是直接刷新到主内存中,读的内存语义是直接从主内存中读取。
在每一个volatile写操作前面插入一个StoreStore屏障
在每一个volatile写操作后面插入一个StoreLoad屏障
在每一个volatile读操作后面插入一个LoadLoad屏障
在每一个volatile读操作后面插入一个LoadStore屏障
2 Java内存模型中的8种工作内存与主内存之间的原子操作
read(读取)→load(加载)→use(使用)→assign(赋值)→store(存储)→write(写入)→lock(锁定)→unlock(解锁)
1)read: 作用于主内存,将变量的值从主内存传输到工作内存,主内存到工作内存
2)load: 作用于工作内存,将从主内存传输的变量值放入工作内存变量副本中,即数据加载
3)use: 作用于工作内存,将工作内存变量副本的值传递给执行引擎,每当JVM遇到需要该变量的字节码指令时会执行该操作
4)assign: 作用于工作内存,将从执行引擎接收到的值赋值给工作内存变量,每当JVM遇到一个给变量赋值字节码指令时会执行该操作
5)store: 作用于工作内存,将赋值完毕的工作变量的值写回给主内存
6)write: 作用于主内存,将store传输过来的变量值赋值给主内存中的变量。
由于上述只能保证单条指令的原子性,针对多条指令的组合性原子保证,没有大面积加锁,所以,JVM提供了另外两个原子指令:
7)lock: 作用于主内存,将一个变量标记为一个线程独占的状态,只是写(write)时候加锁,就只是锁了写变量的过程。
8)unlock: 作用于主内存,把一个处于锁定状态的变量释放,然后才能被其他线程占用
3 JAVA的可见性和有序性问题介绍
https://www.cnblogs.com/jthr/p/15969561.html
4 volatile怎么保证有序性和可见性
https://www.cnblogs.com/jthr/p/15969561.html 第五段
4.1 volatile保证可见性示例
4.1.1 示例1 不加volatile
public class VolatileTest2 {
static boolean flag = true; //不加volatile,没有可见性
public static void main(String[] args) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t come in");
while (flag) {
}
System.out.println(Thread.currentThread().getName() + "\t flag被修改为false,退出.....");
}, "t1").start();
//暂停2秒钟后让main线程修改flag值
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = false;
System.out.println("main线程修改完成");
}
}
执行结果,在flag被修改为true后
System.out.println(Thread.currentThread().getName() + "\t flag被修改为false,退出.....");这段代码还是没有执行,还在循环里面没有出来
说明flag被主线程修改为false,线程t1并不知道。
1) 主线程修改了flag之后没有将其刷新到主内存,所以t1线程看不到。
2) 主线程将flag刷新到了主内存,但是t1一直读取的是自己工作内存中flag的值,没有去主内存中更新获取flag最新的值
t1 come in
main线程修改完成
4.1.2 示例2,加上volatile
public class VolatileTest2 {
//static boolean flag = true; //不加volatile,没有可见性
static volatile boolean flag = true;
public static void main(String[] args) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + "\t come in");
while (flag) {
}
System.out.println(Thread.currentThread().getName() + "\t flag被修改为false,退出.....");
}, "t1").start();
//暂停2秒钟后让main线程修改flag值
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = false;
System.out.println("main线程修改完成");
}
}
执行结果,flag被修改为false,退出.....打印出来了,说明主线程对flag的修改对线程t1可见。
1) 主线程修改了flag之后立即将其刷新到主内存
2)t1一直读取是去主内存中更新获取flag最新的值
t1 come in
main线程修改完成
t1 flag被修改为false,退出.....
4.2 volatile保证有序性示例
4.2.1 示例1 不加volatile
public class VolatileTest3 {
public static int x,y,a,b;
public static void main(String[] args) throws InterruptedException {
for (long i = 0;i < Long.MAX_VALUE;i++){
CountDownLatch latch = new CountDownLatch(2);
x = 0;
y = 0;
a = 0;
b = 0;
Thread t1 = new Thread(()->{
a = 1;
x = b;
latch.countDown();
});
Thread t2 =new Thread(()->{
b = 1;
y = a;
latch.countDown();
});
t1.start();
t2.start();
latch.await();
if(x == 0 && y == 0){
System.out.println("第" + i + "次执行");
break;
}
}
}
}
执行结果。若是不发生指令重排,那么就不可能出现x=0,y=0的情况。但是出现了,说明发生了指令重排
第65701次执行
4.2.2 加上volatile
public class VolatileTest3 {
public static volatile int x,y,a,b;
public static void main(String[] args) throws InterruptedException {
for (long i = 0;i < Long.MAX_VALUE;i++){
CountDownLatch latch = new CountDownLatch(2);
x = 0;
y = 0;
a = 0;
b = 0;
Thread t1 = new Thread(()->{
a = 1;
x = b;
latch.countDown();
});
Thread t2 =new Thread(()->{
b = 1;
y = a;
latch.countDown();
});
t1.start();
t2.start();
latch.await();
if(x == 0 && y == 0){
System.out.println("第" + i + "次执行");
break;
}
}
}
}
执行结果,发现执行很久,也没有出现x=0,y=0的情况,说明volatile保证了有序性
5 为什么volatile没有保证原子性
5.1 读取赋值一个普通变量的过程
由于上述的八条指令只能保证单条指令的原子性,不能保证多条指令共同执行时的原子性。
如下图,主内存存在一个变量A
线程一对A执行执行一系列的操作read、load、、、store、write
在任意两个操作中间,线程二都有可以对A执行一系列的操作read、load、、、store、write。
5.2 读取赋值一个volatile变量的过程
被volatile修饰的变量保证了read-load-use这三个操作(保证每次使用都是获取的主内存最新的值)的原子性和assign-store-write的原子性(保证每次写完立即把值刷回主内存)
如下图,主内存存在一个变量A
线程一对A执行执行一系列的操作read-load-use、assign-store-write
在read-load-use这三个操作和assign-store-write这三个操作中间,第二个线程有可以对A执行一系列的操作read-load-use、assign-store-write,所以不能保证原子性
5.3 示例
public class VolatileTest1 {
//不能保证原子性演示
private static volatile int a = 0;
public static void main(String[] args) throws InterruptedException {
CountDownLatch c = new CountDownLatch(10);
for (int i = 0;i < 10;i++) {
new Thread(()->{
for (int j = 0;j < 1000;j++) {
++a;
}
c.countDown();
}).start();
}
c.await();
System.out.println(a);
}
}
执行结果,期待结果是10000,结果不到10000,没有保证原子性
9789