AtomicInteger的实际用途


Answers:


189

有以下两个主要用途AtomicInteger

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

  • 作为支持比较和交换指令(compareAndSet())来实现非阻塞算法的原语。

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


1
我想我了解第一个用途。这是为了确保在再次访问属性之前计数器已增加。正确?您能举一个第二个例子的简短例子吗?
James P.

8
您对首次使用的理解是正确的-它只是确保如果另一个线程修改了readwrite that value + 1操作之间的计数器,则将检测到该情况,而不是覆盖旧的更新(避免“丢失的更新”问题)。这实际上是一种特殊情况compareAndSet-如果旧值是2,则该类实际调用compareAndSet(2, 3)-因此,如果与此同时另一个线程修改了该值,则递增方法实际上从头开始重新启动。
Andrzej Doyle

3
“余数> 0?余数:余数+ n;” 在此表达式中,是否有理由在n为0时将余数添加到n?
sandeepkunkunuru

100

我能想到的最简单的绝对示例是使原子操作递增。

使用标准整数:

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++并记住(或不记住)事先获取正确的监视器集要难得多。


2
感谢您的明确解释。在所有方法都已同步的类上使用AtomicInteger有什么好处?后者会被视为“较重”吗?
James P.

3
从我的角度来看,主要是使用AtomicIntegers获得的封装-同步完全在您需要的内容上进行,并且您获得了公共API中的描述性方法来解释预期的结果。(再加上某种程度上您是对的,通常可能最终只是简单地同步了一个类中的所有方法,而该类可能太粗粒度了,尽管通过HotSpot执行锁优化和防止过早优化的规则,我认为可读性是一种胜于性能。)
Andrzej Doyle

这是非常清晰准确的解释,谢谢!
Akash5288 '17

最后,一个解释为我正确解决了。
Benny Bottema

58

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


37

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

最简单的是getAndXXXor 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可以用作s中的可变版本,Integer例如Maps作为值。


1
我不希望将其AtomicInteger用作地图键,因为它使用默认equals()实现,几乎可以肯定这不是在地图中使用语义所期望的。
Andrzej Doyle

1
@Andrzej可以肯定,它不是一个必须是不可变量的键,而是一个值。
gabuzo 2011年

@gabuzo知道为什么原子整数在同步时表现良好吗?
Supun Wijerathne '17

该测试现在已经很旧了(超过6年),也许我对最近的JRE重新测试很感兴趣。我对AtomicInteger的回答还不够深入,但是由于这是一项非常具体的任务,因此它将使用仅在此特定情况下有效的同步技术。还请注意,该测试是单线程的,并且在重负载的环境中进行类似的测试可能不会给AtomicInteger带来如此明显的胜利
gabuzo

我相信它的3毫秒和5.5毫秒
Sathesh

17

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


感谢您分享这个实际的例子。这听起来像是我应该使用的东西,因为我需要为导入到程序中的每个文件提供唯一的ID :)
James P.

7

就像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


5

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


4

您可以在原子整数或long上使用compareAndSwap(CAS)实现非阻塞锁。该“TL2”软件事务内存阐述这一点:

我们将特殊版本的写锁与每个事务存储位置相关联。以最简单的形式,版本化的写锁是一个单字自旋锁,它使用CAS操作来获取该锁,并使用存储来释放它。由于只需要一个位来表示已采取锁定,因此我们使用其余的锁定字来保存版本号。

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

本文介绍了处理器具有对比较和交换操作的硬件支持,这使得处理效率非常高。它还声称:

在低到中度竞争中,使用原子变量的基于CAS的无阻塞计数器比基于锁的计数器具有更好的性能


3

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


我懂了。这是在属性或实例充当应用程序内部的某种全局变量的情况下发生的吗?还是您可以想到其他情况?
James P.

1

我使用AtomicInteger解决了餐厅哲学家的问题。

在我的解决方案中,使用AtomicInteger实例表示分叉,每个哲学家需要两个实例。每个哲学家都被标识为1到5的整数。当一个哲学家使用一个分叉时,AtomicInteger持有该哲学家的值,即1到5,否则不使用该叉子,因此AtomicInteger的值是-1。 。

然后,AtomicInteger允许在一个原子操作中检查一个fork是否为free,value ==-1,并将其设置为fork的所有者(如果为free)。请参见下面的代码。

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问题,就像流程需要资源才能继续工作一样。


0

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.