【发布时间】:2011-06-16 15:56:13
【问题描述】:
我有点理解 AtomicInteger 和其他 Atomic 变量允许并发访问。但是这个类通常在什么情况下使用?
【问题讨论】:
标签: java concurrency atomic
我有点理解 AtomicInteger 和其他 Atomic 变量允许并发访问。但是这个类通常在什么情况下使用?
【问题讨论】:
标签: java concurrency atomic
AtomicInteger主要有两个用途:
作为一个原子计数器(incrementAndGet() 等),可以被多个线程同时使用
作为支持compare-and-swap指令(compareAndSet())的原语实现非阻塞算法。
这是来自Brian Göetz's Java Concurrency In Practice 的非阻塞随机数生成器示例:
public class AtomicPseudoRandom extends PseudoRandom {
private AtomicInteger seed;
AtomicPseudoRandom(int seed) {
this.seed = new AtomicInteger(seed);
}
public int nextInt(int n) {
while (true) {
int s = seed.get();
int nextSeed = calculateNext(s);
if (seed.compareAndSet(s, nextSeed)) {
int remainder = s % n;
return remainder > 0 ? remainder : remainder + n;
}
}
}
...
}
如您所见,它的工作方式与incrementAndGet() 基本相同,但执行的是任意计算(calculateNext())而不是递增(并在返回之前处理结果)。
【讨论】:
read 和write that value + 1 操作之间的计数器,则会检测到这一点,而不是覆盖旧的更新(避免“丢失更新”问题)。这实际上是compareAndSet 的一个特例——如果旧值是2,则该类实际上调用compareAndSet(2, 3)——所以如果另一个线程在此期间修改了该值,则增量方法有效地从头开始。
我能想到的最简单的例子是增加一个原子操作。
使用标准整数:
private volatile int counter;
public int getNextUniqueIndex() {
return counter++; // Not atomic, multiple threads could get the same result
}
使用 AtomicInteger:
private AtomicInteger counter;
public int getNextUniqueIndex() {
return counter.getAndIncrement();
}
后者是一种非常简单的方法来执行简单的突变效果(尤其是计数或唯一索引),而不必诉诸于同步所有访问。
更复杂的无同步逻辑可以通过使用compareAndSet()作为一种乐观锁定来使用——获取当前值,基于此计算结果,设置此结果iff值仍然是输入用于进行计算,否则重新开始 - 但计数示例非常有用,如果有任何涉及多个线程的提示,我将经常使用 AtomicIntegers 进行计数和 VM 范围的唯一生成器,因为它们是使用起来非常容易,我几乎认为使用普通 ints 是过早的优化。
虽然您几乎总是可以使用ints 和适当的synchronized 声明实现相同的同步保证,但AtomicInteger 的美妙之处在于线程安全性内置于实际对象本身中,您无需担心关于碰巧访问int 值的每种方法的可能交错和所持有的监视器。调用 getAndIncrement() 时意外违反线程安全要比返回 i++ 并记住(或不)事先获取正确的监视器集要困难得多。
【讨论】:
如果您查看 AtomicInteger 的方法,您会注意到它们往往对应于 int 上的常见操作。例如:
static AtomicInteger i;
// Later, in a thread
int current = i.incrementAndGet();
是这个的线程安全版本:
static int i;
// Later, in a thread
int current = ++i;
方法映射如下:++i 是i.incrementAndGet()i++ 是i.getAndIncrement()--i 是i.decrementAndGet()i-- 是i.getAndDecrement()i = x 是 i.set(x)x = i 是 x = i.get()
还有其他方便的方法,例如compareAndSet 或addAndGet
【讨论】:
AtomicInteger 的主要用途是当您处于多线程上下文中并且需要在不使用synchronized 的情况下对整数执行线程安全操作时。基本类型 int 的分配和检索已经是原子的,但是 AtomicInteger 带有许多在 int 上不是原子的操作。
最简单的是getAndXXX 或xXXAndGet。例如getAndIncrement() 是原子等效于i++,它不是原子的,因为它实际上是三个操作的捷径:检索、加法和赋值。 compareAndSet 对于实现信号量、锁、闩锁等非常有用。
使用AtomicInteger 比使用同步执行相同的操作更快、更易读。
一个简单的测试:
public synchronized int incrementNotAtomic() {
return notAtomic++;
}
public void performTestNotAtomic() {
final long start = System.currentTimeMillis();
for (int i = 0 ; i < NUM ; i++) {
incrementNotAtomic();
}
System.out.println("Not atomic: "+(System.currentTimeMillis() - start));
}
public void performTestAtomic() {
final long start = System.currentTimeMillis();
for (int i = 0 ; i < NUM ; i++) {
atomic.getAndIncrement();
}
System.out.println("Atomic: "+(System.currentTimeMillis() - start));
}
在我的装有 Java 1.6 的 PC 上,原子测试在 3 秒内运行,而同步测试在大约 5.5 秒内运行。这里的问题是同步操作(notAtomic++)非常短。所以与操作相比,同步的成本真的很重要。
除了原子性之外,AtomicInteger 可以用作Integer 的可变版本,例如在Maps 中作为值。
【讨论】:
AtomicInteger 作为映射键,因为它使用默认的 equals() 实现,这几乎肯定不是你所期望的语义如果在地图中使用。
例如,我有一个生成某个类的实例的库。这些实例中的每一个都必须具有唯一的整数 ID,因为这些实例代表正在发送到服务器的命令,并且每个命令都必须具有唯一的 ID。由于允许多个线程同时发送命令,我使用 AtomicInteger 来生成这些 ID。另一种方法是使用某种锁和一个常规整数,但这既慢又不优雅。
【讨论】:
就像 gabuzo 所说,有时我想通过引用传递一个 int 时使用 AtomicIntegers。它是一个具有特定于架构的代码的内置类,因此它比我可以快速编写的任何 MutableInteger 更容易并且可能更优化。话虽如此,这感觉就像是对班级的滥用。
【讨论】:
在 Java 8 中,原子类扩展了两个有趣的功能:
两者都使用 updateFunction 来执行原子值的更新。不同之处在于第一个返回旧值,第二个返回新值。可以实现 updateFunction 以执行比标准操作更复杂的“比较和设置”操作。例如它可以检查原子计数器不低于零,通常它需要同步,这里的代码是无锁的:
public class Counter {
private final AtomicInteger number;
public Counter(int number) {
this.number = new AtomicInteger(number);
}
/** @return true if still can decrease */
public boolean dec() {
// updateAndGet(fn) executed atomically:
return number.updateAndGet(n -> (n > 0) ? n - 1 : n) > 0;
}
}
代码取自Java Atomic Example。
【讨论】:
当我需要为可以从多个线程访问或创建的对象提供 Id 时,我通常使用 AtomicInteger,并且我通常将它用作我在对象的构造函数中访问的类的静态属性。
【讨论】:
您可以在原子整数或长整数上使用 compareAndSwap (CAS) 实现非阻塞锁。 "Tl2" Software Transactional Memory 论文对此进行了描述:
我们将一个特殊的版本化写锁与每个事务相关联 内存位置。在其最简单的形式中,版本化写锁是一个 使用 CAS 操作获取锁的单字自旋锁和 一个商店来发布它。因为只需要一个位来指示 锁定被占用,我们使用锁定字的其余部分来保存 版本号。
它描述的是首先读取原子整数。将其拆分为忽略的锁定位和版本号。尝试 CAS 将其作为锁定位清除,并使用当前版本号写入锁定位集和下一个版本号。循环直到你成功并且你是拥有锁的线程。通过设置当前版本号并清除锁定位来解锁。该论文描述了使用锁中的版本号来协调线程在写入时具有一致的读取集。
This article 描述了处理器对比较和交换操作具有硬件支持,从而非常高效。它还声称:
使用原子变量的基于 CAS 的非阻塞计数器具有更好的性能 在中低竞争中的性能优于基于锁的计数器
【讨论】:
关键是它们允许安全地并发访问和修改。它们通常用作多线程环境中的计数器 - 在引入之前,这必须是一个用户编写的类,将各种方法封装在同步块中。
【讨论】:
我使用 AtomicInteger 解决了餐饮哲学家的问题。
在我的解决方案中,AtomicInteger 实例用于表示分叉,每个哲学家需要两个。每个 Philosopher 被标识为一个整数,从 1 到 5。当哲学家使用分叉时,AtomicInteger 保存哲学家的值,从 1 到 5,否则不使用分叉,因此 AtomicInteger 的值为 -1 .
然后,AtomicInteger 允许在一个原子操作中检查分叉是否空闲,值 ==-1,如果空闲,则将其设置为分叉的所有者。请参阅下面的代码。
AtomicInteger fork0 = neededForks[0];//neededForks is an array that holds the forks needed per Philosopher
AtomicInteger fork1 = neededForks[1];
while(true){
if (Hungry) {
//if fork is free (==-1) then grab it by denoting who took it
if (!fork0.compareAndSet(-1, p) || !fork1.compareAndSet(-1, p)) {
//at least one fork was not succesfully grabbed, release both and try again later
fork0.compareAndSet(p, -1);
fork1.compareAndSet(p, -1);
try {
synchronized (lock) {//sleep and get notified later when a philosopher puts down one fork
lock.wait();//try again later, goes back up the loop
}
} catch (InterruptedException e) {}
} else {
//sucessfully grabbed both forks
transition(fork_l_free_and_fork_r_free);
}
}
}
因为 compareAndSet 方法没有阻塞,它应该增加吞吐量,完成更多的工作。您可能知道,当需要控制对资源的访问时使用哲学家就餐问题,即需要分叉,就像一个进程需要资源来继续工作一样。
【讨论】:
compareAndSet() 函数的简单示例:
import java.util.concurrent.atomic.AtomicInteger;
public class GFG {
public static void main(String args[])
{
// Initially value as 0
AtomicInteger val = new AtomicInteger(0);
// Prints the updated value
System.out.println("Previous value: "
+ val);
// Checks if previous value was 0
// and then updates it
boolean res = val.compareAndSet(0, 6);
// Checks if the value was updated.
if (res)
System.out.println("The value was"
+ " updated and it is "
+ val);
else
System.out.println("The value was "
+ "not updated");
}
}
打印出来的是: 以前的值:0 值已更新,为 6 另一个简单的例子:
import java.util.concurrent.atomic.AtomicInteger;
public class GFG {
public static void main(String args[])
{
// Initially value as 0
AtomicInteger val
= new AtomicInteger(0);
// Prints the updated value
System.out.println("Previous value: "
+ val);
// Checks if previous value was 0
// and then updates it
boolean res = val.compareAndSet(10, 6);
// Checks if the value was updated.
if (res)
System.out.println("The value was"
+ " updated and it is "
+ val);
else
System.out.println("The value was "
+ "not updated");
}
}
打印出来的是: 以前的值:0 值未更新
【讨论】: