原子类型的动机

实现多线程应用程序的简单方法是使用 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 对象上进行大量同时调用,则同步可能会成为瓶颈。特别:

  1. 每个 synchronized 方法调用将从当前线程获取 Counters 实例的锁定开始。
  2. 线程将在检查 number 值时保持锁定并更新计数器。
  3. 最后,它将释放锁,允许其他线程访问。

如果一个线程尝试获取锁定而另一个线程持有该锁定,则在步骤 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 中的偏置锁定