AtomicInteger的实际用途

我理解AtomicInteger和其他原子变量允许并发访问。在什么情况下通常使用这个类?

236211 次浏览

AtomicInteger有两个主要用途:

  • 作为一个原子计数器(incrementAndGet()等),可以被许多线程并发使用

  • 作为一个原语,支持比较指令(compareAndSet())来实现非阻塞算法。

    下面是一个来自Brian Göetz的Java并发实践的非阻塞随机数生成器的例子:

    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())而不是增量(并在返回之前处理结果)

关键是它们允许安全的并发访问和修改。它们通常在多线程环境中用作计数器——在引入它们之前,这必须是一个用户编写的类,将各种方法包装在同步块中。

我能想到的最简单的例子是使递增成为一个原子操作。

使用标准int型:

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()作为一种乐观锁定类型来使用——获得当前值,基于此计算结果,设置此结果敌我识别值仍然是用于进行计算的输入,否则重新开始——但计数示例非常有用,并且我经常使用AtomicIntegers进行计数,如果有任何涉及多个线程的提示,vm范围内的唯一生成器。因为它们很容易使用,我几乎认为使用普通的ints是不成熟的优化。

虽然你几乎总是可以通过ints和适当的synchronized声明来实现相同的同步保证,但AtomicInteger的美妙之处在于线程安全是内置在实际对象本身的,而不是你需要担心可能的交错和持有的监视器,每个方法都会访问int值。调用getAndIncrement()时不小心违反线程安全要比返回i++并记得(或不记得)事先获取正确的监视器集更难。

例如,我有一个生成某些类实例的库。每个实例必须有一个唯一的整数ID,因为这些实例表示发送到服务器的命令,并且每个命令必须有一个唯一的ID。由于允许多个线程并发发送命令,所以我使用AtomicInteger来生成这些id。另一种方法是使用某种锁和常规整数,但这既慢又不优雅。

AtomicInteger的主要用途是当您处于多线程上下文中,并且需要在不使用synchronized的情况下对整数执行线程安全操作时。原语类型int上的赋值和检索已经是原子的,但是AtomicInteger附带了许多在int上不是原子的操作。

最简单的是getAndXXXxXXAndGet。例如,getAndIncrement()是与i++的原子等价物,而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具有的方法,您将注意到它们倾向于对应于对int的常见操作。例如:

static AtomicInteger i;


// Later, in a thread
int current = i.incrementAndGet();

是线程安全的版本:

static int i;


// Later, in a thread
int current = ++i;
方法映射如下:
++ii.incrementAndGet()
i++i.getAndIncrement()
--ii.decrementAndGet()
i--i.getAndDecrement()
i = xi.set(x)
x = ix = i.get()

还有其他方便的方法,如compareAndSetaddAndGet

就像gabuzo说的,当我想通过引用传递一个整型时,有时我使用AtomicIntegers。它是一个内置类,具有特定于体系结构的代码,因此它比我可以快速编写的任何MutableInteger更容易,也可能更优化。也就是说,这感觉像是对课程的滥用。

可以在原子整数或长值上使用compareAndSwap (CAS)实现非阻塞锁。“Tl2”软件事务内存论文这样描述:

我们为每个事务关联一个特殊版本的写锁 内存位置。在其最简单的形式中,版本化写锁是 使用CAS操作获取锁和的单字自旋锁 一个发布它的商店。因为我们只需要一个比特来表示 如果锁已被占用,则使用锁字的其余部分来保存 版本号。< / p >

它描述的是首先读取原子整数。将其拆分为一个被忽略的锁定位和版本号。尝试CAS将其写入已清除的锁位,并使用当前版本号将其写入锁位设置和下一个版本号。循环直到成功,并且你的线程拥有锁。通过设置当前版本号并清除锁定位来解锁。本文描述了使用锁中的版本号来协调线程在写入时具有一致的读取集。

这篇文章描述了处理器有硬件支持比较和交换操作,使得非常高效。它还声称:

非阻塞基于cas的计数器使用原子变量有更好的 在低到中等竞争情况下,性能优于基于锁的计数器

当我需要为可以从多个线程访问或创建的对象提供id时,我通常使用AtomicInteger,并且我通常将它用作我在对象的构造函数中访问的类的静态属性。

在Java 8中,原子类扩展了两个有趣的函数:

  • getAndUpdate(IntUnaryOperator updateFunction)
  • int updateAndGet(IntUnaryOperator updateFunction)

两者都使用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原子示例

我使用AtomicInteger来解决就餐哲学家的问题。

在我的解决方案中,使用AtomicInteger实例来表示fork,每个哲学家需要两个。每个哲学家都被标识为一个整数,从1到5。当一个哲学家使用一个fork时,AtomicInteger保存哲学家的值,从1到5,否则该fork没有被使用,因此AtomicInteger的值为-1。

AtomicInteger允许在一个原子操作中检查一个fork是否空闲,value==-1,如果空闲则将其设置为fork的所有者。参见下面的代码。

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方法不阻塞,它应该增加吞吐量,完成更多的工作。正如你所知道的,Dining Philosophers问题是在需要对资源进行受控访问时使用的,即需要fork,就像一个进程需要资源来继续工作一样。

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 日志含义未更新值

原子类不是java.lang.Integer和相关类的通用替代品。它们没有定义equals、hashCode和compareTo等方法。(因为原子变量预计会发生变化,所以它们是哈希表键的糟糕选择。)此外,仅为在预期应用程序中通常有用的类型提供类。例如,没有用于表示字节的原子类。在您希望这样做的不常见情况下,可以使用AtomicInteger保存字节值,并进行适当的强制转换。你也可以使用float . floattorawintbits (float)和float . intbitstfloloat (int)转换持有浮点数,使用double . doubletorawlongbits (double)和double . longbitstodouble (long)转换持有双精度浮点数。

参考:https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/atomic/package-summary.html