所谓原子性操作,是指执行一系列操作时,这些操作要么全部执行,要么全部不执行,不存在只执行其中一部分的情况。在设计计数器时一般都先读取当前值,然后 +1,再更新。这个过程是读—改—写的过程,如果不能保证这个过程是原子性的,那么就会出现线程安全问题。如下代码是线程不安全的,因为不能保证 ++value 是原子性操作。
public class ThreadNotSafeCount {
private Long value;
public Long getCount() {
return value;
}
public void inc() {
++value;
}
}
使用 Javap -c 命令查看汇编代码,如下所示。
public void inc();
Code:
0: aload_0
1: dup
2: getfield #2 // Field value:J
5: lconst_1
6: ladd
7: putfield #2 // Field value:J
10: return
由此可见,简单的 ++value 由 2、5、6、7 四步组成,其中第 2 步是获取当前 value 的值并放入栈顶,第 5 步把常量 1 放入栈顶,第 6 步把当前栈顶中两个值相加并把结果放入栈顶,第 7 步则把栈顶的结果赋给 value 变量。因此,Java 中简单的一句 ++value 被转换为汇编后就不具有原子性了。
那么如何才能保证多个操作的原子性呢?最简单的方法就是使用 synchronized 关键字进行同步,修改代码如下。
public class ThreadSafeCount {
private Long value;
public synchronized Long getCount() {
return value;
}
public synchronized void inc() {
++value;
}
}
使用 synchronized 关键字的确可以实现线程安全性,即内存可见性和原子性,但是 synchronized 是独占锁,没有获取内部锁的线程会被阻塞掉,而这里的 getCount 方法只是读操作,多个线程同时调用不会存在线程安全问题。但是加了关键字 synchronized 后,同一时间就只能有一个线程可以调用,这显然大大降低了并发性。你也许会问,既然是只读操作,那为何不去掉 getCount 方法上的 synchronized 关键字呢?其实是不能去掉的,别忘了这里要靠 synchronized 来实现 value 的内存可见性。那么有没有更好的实现呢?答案是肯定的,下面将讲到的在内部使用非阻塞 CAS 算法实现的原子性操作类 AtomicLong 就是一个不错的选择。