写点什么

浅析 Java 内存模型 三

用户头像
朱华
关注
发布于: 2020 年 10 月 11 日

volatile 关键字



volatile 是什么

volatile 是一种同步机制,比 synchronized 或者 Lock 相关类更轻量,因为使用 volatile 并不会发生上下文切换等开销很大的行为。

如果一个变量被修饰成 volatile,那么 JVM 就知道了这个变量可能会被并发修改。

但是开销小,相对的能力也小,虽然说 volatile 是用来同步的保证线程安全的,但是 volatile 做不到 synchronized 那样的原子保护,volatile 仅在很有限的场景下才能发挥作用。



volatile 的使用场合

  • 不适用 a++ 的场合

public class NoVolatile implements Runnable {
int a;
AtomicInteger realA = new AtomicInteger();
public static void main(String[] args) throws InterruptedException {
Runnable r = new NoVolatile();
Thread thread1 = new Thread(r);
Thread thread2 = new Thread(r);
thread1.start();
thread2.start();
thread1.join();
thread2.join();
System.out.println(((NoVolatile) r).a);
System.out.println(((NoVolatile) r).realA.get());
}
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
a++;
realA.incrementAndGet();
}
}
}

上述代码的结果并不是我们想象的是 20000,而是一个比它小的数。这是因为 a++ 运算不是原子性的操作。



  • 适合场景 1:boolean flag

如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以用 volatile 来代替 synchronized 或者代替原子变量,因为赋值自身是由原子性的,而 volatile 有保证了可见性,所以就足以保证线程安全。

public class NoVolatile2 implements Runnable {
volatile boolean done = false;
@Override
public void run() {
for (int i = 0; i < 10000; i++) {
setDone();
realA.incrementAndGet();
}
}
private void setDone() {
done = true;
}
}



  • 适用场景 2:作为刷新之前变量的触发器

public class FieldVisibilityABCD {
int a = 1;
volatile int d = 2;
private void change() {
a = 99;
}
private void print() {
a = 6;
a = 5;
a = 8;
d = 7;
System.out.println("a = " + a);
}
public static void main(String[] args) {
FieldVisibilityABCD test = new FieldVisibilityABCD();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.change();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.print();
}
}).start();
}
}



volatile 的作用:可见性、禁止重排序



volatile 的两点作用:

  1. 可见性:读一个 volatile 变量之前,需要先使用相应的本地缓存失效,这样就必须到主内存读取最新的值,写一个 volatile 属性会立即刷入到主内存

  2. 禁止指令重排序优化:解决单例双重锁乱序问题。



volatile 与 synchronized 的关系



volatile 在这方面可以看做是轻量版的 synchronized

如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以用 volatile 来代替 synchronized 或者代替原子变量,因为赋值自身是有原子性的,而 volatile 有保证了可见性,所以就足以保证线程安全。



利用 volatile 解决重排序问题



public class OutOfOrderExecution {
private volatile static int x = 0, y = 0;
private volatile static int a = 0, b = 0;
public static void main(String[] args) throws InterruptedException {
CountDownLatch latch = new CountDownLatch(3);
Thread one = new Thread(new Runnable() {
@Override
public void run() {
try {
latch.countDown();
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
a = 1;
x = b;
}
});
Thread two = new Thread(new Runnable() {
@Override
public void run() {
try {
latch.countDown();
latch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
b = 1;
y = a;
}
});
String result = "第" + i + "次(" + x + "," + y + ")";
if (x == 0 && y == 0) { // 就不可能发生这个情况
System.out.println(result);
} else {
System.out.println(result);
}
}
}



volatile 总结

  1. volatile 修饰符适用于以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值,比如 boolean flag;或者作为触发器,实现轻量级同步。

  2. volatile 属性的读写操作都是无锁的,它不能代替 synchronized,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁上,所以所它是低成本的。

  3. volatile 只能作用于属性,我们用 volatile 修饰属性,这样 compilers 就不会对这个属性做指令重排序

  4. volatile 提供了可见性,任何一个线程对其的修改将立马对其他线程可见。volatile 属性不会被线程缓存,始终从主存中读取。

  5. volatile 提供了 happens-before 保证,对 volatile 变量 v 的写入先行发生于所有其他线程后续对 v 的读操作。

  6. volatile 可以使得 long 和 double 的赋值是原子的。



对 synchronized 可见性的正确理解



synchronized 也可以达到同样的 happens-before 效果

这里关于 synchronized 有一个特别值得注意的点,我们之前可能一致认为,使用 synchronized 之后,synchronized 会帮我们建立临界区,这样在一个线程操作数据的时候,另一个线程无法进来同时操作,所以保证了线程安全。其实这是不全面的,这种说法没有考虑到可见性问题,真正完整的说法是:

synchronized 不仅防止了一个线程在操作某对象是受到其他线程的干扰,同时还保证了修改好之后,可以立即被其他线程所看到。(因为如果被其他线程看不到,那也会有线程安全问题)



  • synchronized 不仅保证了原子性,还保证了可见性。

  • synchronized 不仅让被保护的代码安全,还近朱者赤。



public class FieldVisibilityABCD {
int a = 1;
int b = 2;
int c = 2;
volatile int d = 2;
private void change() {
a = 3;
b = 4;
c = 5;
synchronized (this) {
d = 6;
}
}
private void print() {
synchronized (this) {
int aa = a;
}
int bb = b;
int cc = c;
int dd = d;
System.out.println("b=" + b + ";a=" + a);
}
public static void main(String[] args) {
FieldVisibilityABCD test = new FieldVisibilityABCD();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.change();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.print();
}
}).start();
}
}



发布于: 2020 年 10 月 11 日阅读数: 43
用户头像

朱华

关注

见自己,见天地,见众生。 2018.08.07 加入

还未添加个人简介

评论

发布
暂无评论
浅析 Java 内存模型 三