AtomicInteger 的实际用途

Posted

技术标签:

【中文标题】AtomicInteger 的实际用途【英文标题】:Practical uses for AtomicInteger 【发布时间】:2011-06-16 15:56:13 【问题描述】:

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

【问题讨论】:

【参考方案1】:

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

【讨论】:

我想我理解第一次使用。这是为了确保在再次访问属性之前计数器已经增加。正确的?你能举一个关于第二次使用的简短例子吗? 您对第一次使用的理解是正确的——它只是确保如果另一个线程修改了readwrite that value + 1 操作之间的计数器,则会检测到这一点,而不是覆盖旧的更新(避免“丢失更新”问题)。这实际上是compareAndSet 的一个特例——如果旧值是2,则该类实际上调用compareAndSet(2, 3)——所以如果另一个线程在此期间修改了该值,则增量方法有效地从头开始。 "余数 > 0 ? 余数:余数 + n;"在这个表达式中,当 n 为 0 时,是否有理由将余数加到?【参考方案2】:

我能想到的最简单的例子是增加一个原子操作。

使用标准整数:

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 有什么优势?后者会被认为“更重”吗? 从我的角度来看,这主要是您使用 AtomicIntegers 获得的封装 - 同步发生在您需要的确切位置,并且您获得公共 API 中的描述性方法来解释预期结果是什么。 (在某种程度上你是对的,通常一个人最终会简单地同步一个可能过于粗粒度的类中的所有方法,尽管 HotSpot 执行锁定优化和反对过早优化的规则,我认为可读性是比性能更大的好处。) 这个解释非常清楚准确,谢谢!! 最后一个解释正确地为我清除了。【参考方案3】:

如果您查看 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

【讨论】:

【参考方案4】:

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

最简单的是getAndXXXxXXAndGet。例如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() 实现,这几乎肯定不是你所期望的语义如果在地图中使用。 @Andrzej 当然,不是作为不可变的键,而是一个值。 @gabuzo 知道为什么原子整数在同步方面表现出色吗? 该测试现在已经很老了(超过 6 年),用最近的 JRE 重新测试我可能会很有趣。我没有深入了解 AtomicInteger 来回答,但由于这是一项非常具体的任务,它将使用仅在这种特定情况下有效的同步技术。另请注意,该测试是单线程的,在重负载环境中进行类似的测试可能不会为 AtomicInteger 带来如此明显的胜利 我相信它的 3 毫秒和 5.5 毫秒【参考方案5】:

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

【讨论】:

感谢分享这个实际例子。这听起来像是我应该使用的东西,因为我需要为导入到程序中的每个文件都有唯一的 id :)【参考方案6】:

就像 gabuzo 所说,有时我想通过引用传递一个 int 时使用 AtomicIntegers。它是一个具有特定于架构的代码的内置类,因此它比我可以快速编写的任何 MutableInteger 更容易并且可能更优化。话虽如此,这感觉就像是对班级的滥用。

【讨论】:

【参考方案7】:

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

int 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 Atomic Example。

【讨论】:

【参考方案8】:

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

【讨论】:

【参考方案9】:

您可以在原子整数或长整数上使用 compareAndSwap (CAS) 实现非阻塞锁。 "Tl2" Software Transactional Memory 论文对此进行了描述:

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

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

This article 描述了处理器对比较和交换操作具有硬件支持,从而非常高效。它还声称:

使用原子变量的基于 CAS 的非阻塞计数器具有更好的性能 在中低竞争中的性能优于基于锁的计数器

【讨论】:

【参考方案10】:

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

【讨论】:

我明白了。这是在属性或实例充当应用程序内的一种全局变量的情况下吗?或者还有其他你能想到的案例吗?【参考方案11】:

我使用 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 方法没有阻塞,它应该增加吞吐量,完成更多的工作。您可能知道,当需要控制对资源的访问时使用哲学家就餐问题,即需要分叉,就像一个进程需要资源来继续工作一样。

【讨论】:

【参考方案12】:

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 值未更新

【讨论】:

以上是关于AtomicInteger 的实际用途的主要内容,如果未能解决你的问题,请参考以下文章

concurrent.atomic包下的类AtomicInteger的使用

java中AtomicInteger的使用

对Java原子类AtomicInteger实现原理的一点总结

[Android Pro] AtomicInteger的用法

AtomicInteger lazySet 与 set

三atomic