原子操作出现的先决条件
让我们看一下这个示例,以帮助您了解原子操作的工作原理:
public class Counter {
int count;
public void increment() {
count++;
}
}
当我们有一个线程时,一切都很好,但是如果我们添加多线程,我们会得到错误的结果,这都是因为增量操作不是一个操作,而是三个:获取当前值的请求数数,然后将其递增 1 并再次写入数数.
当两个线程想要增加一个变量时,您很可能会丢失数据。也就是说,两个线程都收到 100,因此,两者都将写入 101 而不是预期值 102。
以及如何解决?你需要使用锁。synchronized关键字有助于解决这个问题,使用它可以保证一个线程一次访问该方法。
public class SynchronizedCounterWithLock {
private volatile int count;
public synchronized void increment() {
count++;
}
}
另外,您需要添加volatile关键字,以确保线程间引用的正确可见性。我们在上面回顾了他的工作。
但仍有缺点。最大的一个是性能,在那个时间点,当许多线程试图获取锁并且一个获得写机会时,其余线程将被阻塞或挂起,直到线程被释放。
所有这些进程、阻塞、切换到另一个状态对于系统性能来说都是非常昂贵的。
原子操作
该算法使用低级机器指令,例如比较和交换(CAS,compare-and-swap,确保数据完整性并且已经有大量研究)。
典型的 CAS 操作对三个操作数进行操作:
- 工作内存空间(M)
- 变量的现有预期值 (A)
- 要设置的新值 (B)
CAS 以原子方式将 M 更新为 B,但前提是 M 的值与 A 相同,否则不采取任何操作。
在第一种和第二种情况下,都会返回M的值,这样可以将获取值、比较值、更新值三个步骤结合起来。这一切都变成了机器级别的一次操作。
当多线程应用程序访问变量并尝试更新它并应用 CAS 时,其中一个线程将获取它并能够更新它。但与锁不同的是,其他线程只会收到无法更新值的错误。然后他们将继续进行进一步的工作,而这种工作完全排除了转换。
在这种情况下,逻辑变得更加困难,因为我们必须处理 CAS 操作未成功运行的情况。我们将只对代码建模,以便在操作成功之前它不会继续。
原子类型介绍
您是否遇到过需要为最简单的int类型变量设置同步的情况?
我们已经介绍过的第一种方法是使用volatile + synchronized。但也有特殊的 Atomic* 类。
如果我们使用 CAS,那么与第一种方法相比,操作工作得更快。此外,我们还有特殊且非常方便的方法来添加值以及递增和递减操作。
AtomicBoolean、 AtomicInteger、 AtomicLong、 AtomicIntegerArray、 AtomicLongArray是其中操作是原子的类。下面我们就和他们一起分析一下工作。
原子整数
AtomicInteger类除了提供扩展的原子操作外,还提供对可以原子方式读取和写入的int值的操作。
它具有get和set方法,其工作方式类似于读取和写入变量。
也就是说,“先于发生”与我们之前讨论过的相同变量的任何后续接收。原子compareAndSet方法也具有这些内存一致性功能。
所有返回新值的操作都是原子执行的:
int addAndGet (int 增量) | 将特定值添加到当前值。 |
布尔比较和设置(预期 int,更新 int) | 如果当前值与预期值匹配,则将值设置为给定的更新值。 |
int decrementAndGet() | 将当前值减一。 |
int getAndAdd(int 增量) | 将给定值添加到当前值。 |
int getAndDecrement() | 将当前值减一。 |
int getAndIncrement() | 将当前值增加一。 |
int getAndSet(int newValue) | 设置给定值并返回旧值。 |
int incrementAndGet() | 将当前值增加一。 |
惰性集(int newValue) | 最后设置为给定值。 |
布尔 weakCompareAndSet(预期,更新 int) | 如果当前值与预期值匹配,则将值设置为给定的更新值。 |
例子:
ExecutorService executor = Executors.newFixedThreadPool(5);
IntStream.range(0, 50).forEach(i -> executor.submit(atomicInteger::incrementAndGet));
executor.shutdown();
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.HOURS);
System.out.println(atomicInteger.get()); // prints 50
GO TO FULL VERSION