【问题标题】:AtomicInteger incrementation not behaving as expectedAtomicInteger 增量未按预期运行
【发布时间】:2014-02-16 17:31:34
【问题描述】:

我正在阅读有关 AtomicInteger 以及它的操作如何是原子的以及这些属性如何使其对多线程有用。

我编写了以下程序来测试它。

我预计集合的最终大小应该是 1000,因为每个线程循环 500 次,并且假设每次线程调用 getNext() 它应该得到一个唯一的数字。

但输出总是小于 1000。我在这里缺少什么?

public class Sequencer {

private final AtomicInteger i = new AtomicInteger(0);

public int getNext(){
    return i.incrementAndGet();
}

public static void main(String[] args) {

    final Sequencer seq = new Sequencer();

    final Set<Integer> set = new HashSet<Integer>();

    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i=0; i<500; i++)
                set.add(seq.getNext());

        }
    },"T1");
    t1.start();


    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            for (int i=0; i<500; i++)
                set.add(seq.getNext());

        }
    },"T2");

    t2.start();

    try {
        t1.join();
        t2.join();
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

    System.out.println(set.size());

}

}

【问题讨论】:

    标签: java multithreading concurrency atomic atomicinteger


    【解决方案1】:

    您错过了 HashSet 不是线程安全的。此外,集合的属性会删除所有重复的数字,因此如果 AtomicInteger 不是线程安全的,您的测试将失败。

    尝试改用ConcurrentLinkedQueue

    编辑:因为它被问了两次:使用同步集是可行的,但它破坏了使用像原子类这样的无锁算法背后的想法。如果在上面的代码中将集合替换为同步集合,则每次调用 add 时线程都必须阻塞。

    这将有效地将您的应用程序减少到单线程,因为唯一完成的工作是同步发生的。实际上它甚至会比单线程慢,因为synchronized 也会造成损失。因此,如果您想实际使用线程,请尽量避免使用synchronized

    【讨论】:

    • 我试过这个 - final Set&lt;Integer&gt; set = Collections.synchronizedSortedSet(new TreeSet&lt;Integer&gt;()); 并且成功了。谢谢大家。
    • 我明白你的意思。上面使用 Set 的想法是测试 a) AtomicInteger incrementAndGet() 是否是原子的,b) 如果操作是原子的,如果它们是唯一的,set 将返回我 size() 1000。但我没有意识到 Hashset 操作不是线程安全的,会导致这个问题。当我切换到 synchronizedSortedSet 时,我使用 AtomicInteger 和原始 int 进行了测试,发现使用 AtomicInteger 的计数始终为 1000,而使用原始 int 则不一致。但我同意你的观点,如果你将线程安全操作与不必要的同步结合起来,那就达不到目的了。
    【解决方案2】:

    HashSet 不是线程安全的,所以你会遇到问题。如果你非常需要使用 HashSet,你可以使用 Vector 或任何线程安全的集合类,或者顺序运行两个线程。

    t1.start();
    t1.join();
    
    t2.start();
    t2.join();
    

    【讨论】:

    • 如果我按顺序运行两个线程,它将超出我的测试目的:)
    【解决方案3】:

    正如几个答案中提到的,由于 HashSet 不是线程安全的,它失败了。

    首先,为了您的测试,让我们验证 AtomicInteger 确实是线程安全的,然后继续查看您的测试失败的原因。稍微修改你的测试。使用两个哈希集,每个线程一个。最后,在连接之后,通过迭代第二个集合并将其添加到第一个集合中,将第二个集合合并到第一个集合中,这将消除重复项(设置属性)。然后对第一组进行计数。 计数将是您所期望的。这证明是HashSet不是线程安全的,不是AtomicInteger。

    那么让我们看看哪些方面不是线程安全的。你正在做 onlyf add()s,所以显然 add() 是不是线程安全的操作,导致数字丢失。让我们看一个会丢失数字的伪代码非线程安全 HashMap add() 示例(这显然不是它的实现方式,只是试图说明它可能是非线程安全的一种方式):

    class HashMap {
     int valueToAdd;
     public add(int valueToAdd) {
       this.valueToAdd = valueToAdd;
       addToBackingStore(this.valueToAdd);
     }
    }
    

    如果多个线程调用add(),并且在改变this.valueToAdd后都到达addToBackingStore(),则只添加valueToAdd的最终值,所有其他值都被覆盖并丢失。

    您的测试中可能发生了类似的情况。

    【讨论】:

      【解决方案4】:

      尝试使用同步的集合以这种方式进行。

      public class Sequencer {
      
          private final AtomicInteger i = new AtomicInteger(0);
      
          public static void main(String[] args) {
      
              final Sequencer seq = new Sequencer();
      
              final Set<Integer> notSafe = new HashSet<Integer>();
              final Set<Integer> set = Collections.synchronizedSet(notSafe);
              Thread t1 = new Thread(new Runnable() {
                  @Override
                  public void run() {
                      for (int i = 0; i < 500; i++)
                          set.add(seq.getNext());
      
                  }
              }, "T1");
              t1.start();
      
      
              Thread t2 = new Thread(new Runnable() {
                  @Override
                  public void run() {
                      for (int i = 0; i < 500; i++)
                          set.add(seq.getNext());
      
                  }
              }, "T2");
      
              t2.start();
      
              try {
                  t1.join();
                  t2.join();
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
      
              System.out.println(set.size());
      
          }
      
          public int getNext() {
              return i.incrementAndGet();
          }
      }
      

      【讨论】:

      • 虽然同步集可以工作,但它完全破坏了使用原子无锁算法的理由。
      • 为什么?我认为它不会对第一个 set 只能保存唯一值有任何影响,所以如果你没有 atomicInteger,即使有 synchronizedSet,你也不会很好同步,因为有时它会加 1 并加1 所以设置根本不允许您添加两个相同的值。所以我认为 concurrentHashSet 不会打破这个问题的概念。
      • 我不知道你在说什么,对不起。但我在帖子中添加了一个编辑,也许这会有所帮助。
      • “虽然同步集可以工作,但它完全破坏了使用原子无锁算法的理由。” -> 它不会破坏它。您必须使用 atomic int 或某种锁,因为 '++' 不是原子操作 :) 另一件事是您存储数据。如果您使用 atomic int 并将结果存储在未同步的容器中,它也将根本不起作用,如果所有算法都应该是线程安全的并在多线程环境中工作,则必须使用所有同步和线程安全的东西。 :) 所以基本上说你在我的回答下面的第一个推荐是无用的和毫无意义的。
      猜你喜欢
      • 2021-06-22
      • 2020-08-02
      相关资源
      最近更新 更多