原子性和可见性
原子性一般指原子操作,原子操作不能被线程调度机制中断,一旦操作开始,那么它一定可以在可能发生的上下文切换之前完成。Java语言规范规定了对基本对象(long和double除外)的读写操作是原子的。
不能将原子性和同步划等号!更不能使用原子性来代替同步,当你想使用原子性代替同步写出无锁代码时,思考 Brain Goetz 的建议:
If you can write a high-performance JVM for a modern microprocessor, then you are qualified to think about whether you can avoid synchronizing.
考虑如下几个操作:
1int x = 1; // s1 原子操作
2boolean flag = flase; // s2 原子操作
3int y = x; // s3
4x++; // s4
5double d = 1.9d; // s5
只有前2个操作是原子操作,后面的操作都不是原子操作。
对于s3来说,可以拆分为读取x的值和将y赋值两个操作,虽然这两个操作都是原子的,但是合起来就不是原子操作了;s4就更复杂了;对于double和long类型的变量, JMM(Java Memory Model)规定了对其的写操作分为2步,每步写入32位1,因此也不是原子的。
原子性的误用 #
查看一个误用原子性的例子:
1public class AtomicTest implements Runnable {
2 private int i = 0;
3
4 public int getValue() {
5 // atomic operation
6 return i;
7 }
8
9 private synchronized void increment() {
10 i++;
11 i++;
12 // equals to
13 // i += 2;
14 }
15
16 @Override
17 public void run() {
18 while (true) increment();
19 }
20
21 public static void main(String[] args) throws InterruptedException {
22 AtomicTest at = new AtomicTest();
23 // 线程1
24 new Thread(at).start();
25 TimeUnit.MILLISECONDS.sleep(1);
26 while (true) {
27 // the value can still be odd
28 int value = at.getValue();
29 if (value % 2 != 0) {
30 System.out.println(value);
31 System.exit(0);
32 }
33 }
34 }
35}
36/* output: (sample)
37145881
38*///:~
上例过分高估了原子性的能力,当另一个线程(mian线程)调用getValue()去访问共享变量时,尽管getValue()方法只有一个return语句,是原子性的,但还是获得了一个不希望的结果——奇数,为什么?虽然increment()
方法是同步的,但是getValue()
方法不需要锁即可访问共享域,此时的i可能在一个不稳定的中间状态。
Java内存模型有如下约定2
Java的域都储存在主存(即物理内存)中
Java的工作线程有独立的内存(CPU缓存)
同步保证可见性
原子操作不保证可见性
依据上面的论断,尝试分析这个不稳定状态:increment()方法使用了同步,即increment()每次自增后都将变量i的结果写入主存;由于getValue()是无锁访问i,它可能获取的可能是increment()方法第一次自增的结果。
那么解决办法有:
- 同步getValue()方法;
- 将2步自增换成一步操作(并不能保证每次getValue()获取的都是期望值,只是不再出现奇数罢了);
- 使用原子类
Java SE 5 引入了java.util.concurrent.atomic
包,里面提供了原子性变量类,这些类提供了一些原子性操作,实际应用的不多,但合理应用可以提升应用性能。
不要过分依赖原子类,就像不要过分依赖原子性一样。
谨慎使用原子类 #
可以使用AtomicInteger类对AtomicTest类进行优化,使其得到预期的结果:
1public class AtomicClassTest implements Runnable {
2 private AtomicInteger i = new AtomicInteger(0);
3 public int getValue() {
4 // atomic operation
5 return i.get();
6 }
7
8 /**
9 * 无锁的原因不是因为原子性,而是因为有且只有一个原子操作
10 * 若此处使用
11 * <pre>
12 * i.incrementAndGet();
13 * i.incrementAndGet();
14 * </pre>
15 * 那么依旧和{@link AtomicTest}一样失败
16 */
17 private void increment() {
18 i.addAndGet(2);
19 }
20
21 @Override
22 public void run() {
23 while (true) increment();
24 }
25
26 public static void main(String[] args) throws InterruptedException {
27 AtomicClassTest act = new AtomicClassTest();
28 ExecutorService executor = Executors.newSingleThreadExecutor();
29 executor.execute(act);
30 ScheduledExecutorService s = Executors.newSingleThreadScheduledExecutor();
31 s.schedule(() -> {
32 // 此方法不会主动退出
33 System.out.println("Aborting...");
34 executor.shutdown();
35 s.shutdown();
36 System.exit(0);
37 }, 5, TimeUnit.SECONDS);
38 while (true) {
39 int value = act.getValue();
40 // the value can still be odd
41 if (value % 2 != 0) {
42 System.out.println(value);
43 System.exit(0);
44 }
45 }
46 }
47}
上面的示例中,方法不用同步,获取到的i的值也不会是奇数。
思考这个问题,main线程每次读取的都是最新修改的i么?
不一定
因为原子性并不能保证可见性,main线程也并不能保证每次获取的都是最新的i值。
可见性(volatile) #
在讨论原子性的时候,提到了原子操作并不能保证可见性。什么是可见性?可见性指的是一个变量被被线程修改后,另一个线程能够马上知道这一修改。
Java SE 5 提供了volatile关键字保证可见性,对volatail域的修改会马上写入到主存中,其他线程会的本地缓存会失效而从主存中去读取。
听起来不错,volatile似乎可以解决资源共享的问题,真的是这样么?
遗憾的是,volatile并不能保证原子性:
1public class VolatileIsNotAtomic {
2
3 // 将变量设置为volatile并不能保证并发安全
4 private volatile int sum;
5
6 void increase() {
7 sum++;
8 }
9
10 void multiThread2() throws InterruptedException {
11 for (int i = 0; i < 10; i++) {
12 Thread thread = new Thread(() -> {
13 for (int j = 0; j < 1000; j++) {
14 increase();
15 }
16 });
17 thread.start();
18 }
19 Thread.sleep(3000);
20 System.out.println(sum);
21 }
22
23 public static void main(String[] args) throws InterruptedException {
24 VolatileIsNotAtomic va = new VolatileIsNotAtomic();
25 va.multiThread2();
26 }
27}
28/* output:(sample)
298806
30*///:~
上例中将域设置为volatile并不能解决多线程环境下的资源共享问题,原因在于,volatile只保证了可见性,没有保证共享资源的有序访问。
volatile关键字的使用非常有限,当想使用volatile关键字的时候,需要仔细考量,因为其可能有潜在的多线程风险。
volatiile关键字最著名的应用是在双重检查( double-check-lock ) 单例中:
1public class DoubleCheckSingleton {
2 private static volatile DoubleCheckSingleton instance;
3
4 private DoubleCheckSingleton() {
5 }
6
7 public static DoubleCheckSingleton getInstance() {
8 if (instance == null) {
9 synchronized (DoubleCheckSingleton.class) {
10 // the double check lock
11 if (instance == null) {
12 instance = new DoubleCheckSingleton();
13 }
14 }
15 }
16 return instance;
17 }
18}
更详细的关于volatile关键字的介绍: Java内存模型与volatile关键字