原子类型的动机
实现多线程应用程序的简单方法是使用 Java 的内置同步和锁定原语; 例如 synchronized
关键字。以下示例显示了我们如何使用 synchronized
来累积计数。
public class Counters {
private final int[] counters;
public Counters(int nosCounters) {
counters = new int[nosCounters];
}
/**
* Increments the integer at the given index
*/
public synchronized void count(int number) {
if (number >= 0 && number < counters.length) {
counters[number]++;
}
}
/**
* Obtains the current count of the number at the given index,
* or if there is no number at that index, returns 0.
*/
public synchronized int getCount(int number) {
return (number >= 0 && number < counters.length) ? counters[number] : 0;
}
}
此实现将正常工作。但是,如果你有大量线程在同一 Counters
对象上进行大量同时调用,则同步可能会成为瓶颈。特别:
- 每个
synchronized
方法调用将从当前线程获取Counters
实例的锁定开始。 - 线程将在检查
number
值时保持锁定并更新计数器。 - 最后,它将释放锁,允许其他线程访问。
如果一个线程尝试获取锁定而另一个线程持有该锁定,则在步骤 1 将阻止(停止)尝试线程,直到锁定被释放。如果多个线程正在等待,其中一个线程将获取它,其他线程将继续被阻止。
这可能会导致一些问题:
-
如果对锁有很多争用 (即许多线程试图获取它),那么一些线程可能会被阻塞很长时间。
-
当线程被阻塞等待锁定时,操作系统通常会尝试将执行切换到不同的线程。该上下文切换对处理器产生相对大的性能影响。
-
当同一个锁上有多个线程被阻塞时,不能保证它们中的任何一个都会被公平地处理(即保证每个线程都被安排运行)。这可能导致线程饥饿。
如何实现原子类型?
让我们从使用 AtomicInteger
计数器重写上面的例子开始:
public class Counters {
private final AtomicInteger[] counters;
public Counters(int nosCounters) {
counters = new AtomicInteger[nosCounters];
for (int i = 0; i < nosCounters; i++) {
counters[i] = new AtomicInteger();
}
}
/**
* Increments the integer at the given index
*/
public void count(int number) {
if (number >= 0 && number < counters.length) {
counters[number].incrementAndGet();
}
}
/**
* Obtains the current count of the object at the given index,
* or if there is no number at that index, returns 0.
*/
public int getCount(int number) {
return (number >= 0 && number < counters.length) ?
counters[number].get() : 0;
}
}
我们用 AtomicInteger[]
替换了 int[]
,并在每个元素中用一个实例初始化它。我们还添加了对 incrementAndGet()
和 get()
的调用来代替 int
值的操作。
但最重要的是我们可以删除 synchronized
关键字,因为不再需要锁定。这是有效的,因为 incrementAndGet()
和 get()
操作是原子的和线程安全的。在这种情况下,它意味着:
-
数组中的每个计数器只能在操作的之前状态(如增量)或之后状态下观察到。
-
假设操作发生在时间
T
,没有线程在时间T
之后将能够看到之前状态。
此外,虽然两个线程实际上可能同时尝试更新同一个 AtomicInteger
实例,但操作的实现确保在给定实例上一次只发生一个增量。这是在没有锁定的情况下完成的,通常会带来更好的性
原子类型如何工作?
原子类型通常依赖于目标机器的指令集中的专用硬件指令。例如,基于 Intel 的指令集提供了一个 CAS
( 比较和交换 )指令,它将以原子方式执行特定的存储器操作序列。
这些低级指令用于在相应的 AtomicXxx
类的 API 中实现更高级别的操作。例如,(再次,在类似 C 的伪代码中):
private volatile num;
int increment() {
while (TRUE) {
int old = num;
int new = old + 1;
if (old == compare_and_swap(&num, old, new)) {
return new;
}
}
}
如果 AtomicXxxx
没有争用,则 if
测试将成功,循环将立即结束。如果存在争用,则 if
将对除了一个线程之外的所有线程都失败,并且它们将在循环中旋转以进行少量循环循环。在实践中,旋转速度提高了几个数量级(除了在不切实际的高级别争用中,同步性能优于原子类,因为当 CAS 操作失败时,重试只会增加争用),而不是暂停线程并切换到另一个一。
顺便提一下,JVM 通常使用 CAS 指令来实现无竞争锁定。如果 JVM 可以看到锁当前未被锁定,它将尝试使用 CAS 来获取锁。如果 CAS 成功,则无需进行昂贵的线程调度,上下文切换等。有关所用技术的更多信息,请参阅 HotSpot 中的偏置锁定 。