【问题标题】:How to implement a concurrent circular ticker (counter) in Java?如何在 Java 中实现一个并发的循环代码(计数器)?
【发布时间】:2011-11-26 05:03:41
【问题描述】:

我想在 Java 中实现一个循环计数器。 每个请求的计数器应该增加(原子地),达到上限时应该翻转到 0。

实现这一点的最佳方法是什么?是否有任何现有的实现?

【问题讨论】:

    标签: java concurrency counter atomic


    【解决方案1】:

    AtomicInteger 上实现这样的计数器很容易:

    public class CyclicCounter {
    
        private final int maxVal;
        private final AtomicInteger ai = new AtomicInteger(0);
    
        public CyclicCounter(int maxVal) {
            this.maxVal = maxVal;
        }
    
        public int cyclicallyIncrementAndGet() {
            int curVal, newVal;
            do {
              curVal = this.ai.get();
              newVal = (curVal + 1) % this.maxVal;
            } while (!this.ai.compareAndSet(curVal, newVal));
            return newVal;
        }
    
    }
    

    【讨论】:

    • 我想知道如果线程在this.ai.get()} while (!this.ai.compareAndSet(curVal, newal)); 之间失去优先级,这是否真的是原子的
    【解决方案2】:

    使用 Java 8

    public class CyclicCounter {
    
        private final int maxVal;
        private final AtomicInteger counter = new AtomicInteger(0);
    
        public CyclicCounter(int maxVal) {
          this.maxVal = maxVal;
        }
    
        public long incrementAndGet() {
            return counter.accumulateAndGet(1, (index, inc) -> (++index >= maxVal ? 0 : index));
        }
    
    }
    

    【讨论】:

      【解决方案3】:

      如果您担心使用 CAS 或 synchronized 的争用,那么您可以考虑使用更复杂的方法,例如提议的 JSR 166e LongAddersourcejavadoc)。

      这是一个简单的计数器,多线程访问的争用较少。您可以将其包装以公开(当前值 mod 最大值)。也就是说,根本不存储包装的值。

      【讨论】:

      • 如何使用LongAdder 实现原子翻转为零?
      【解决方案4】:

      如果您使用模数运算符,您可以只增加并返回模数。不幸的是,模运算符很昂贵,所以我鼓励其他对性能很重要的解决方案。

      public class Count {
          private final AtomicLong counter = new AtomicLong();
          private static final long MAX_VALUE = 500;
          public long getCount() {
              return counter.get() % MAX_VALUE;
          }
          public long incrementAndGet(){
              return counter.incrementAndGet() % MAX_VALUE;
      
          }
      }
      

      您还必须解决 Long.MAX_VALUE 的情况。

      【讨论】:

      • counter超过long时你会怎么做?
      【解决方案5】:

      我个人认为AtomicInteger 解决方案有点难看,因为它引入了竞争条件,这意味着您的更新尝试可能“失败”并且必须重复(通过在 while 循环中迭代)使得更新时间不太确定而不是在关键部分执行整个操作。

      编写自己的计数器非常简单,我推荐这种方法。从 OO 的角度来看,它也更好,因为它只公开了您被允许执行的操作。

      public class Counter {
        private final int max;
        private int count;
      
        public Counter(int max) {
          if (max < 1) { throw new IllegalArgumentException(); }
      
          this.max = max;
        }
      
        public synchronized int getCount() {
          return count;
        }
      
        public synchronized int increment() {
          count = (count + 1) % max;
          return count;
        }
      }
      

      编辑

      我认为使用 while 循环解决方案的另一个问题是,如果有大量线程尝试更新计数器,您最终可能会遇到多个活动线程旋转并尝试更新计数器的情况。假设只有 1 个线程会成功,所有其他线程都会失败,从而导致它们迭代并浪费 CPU 周期。

      【讨论】:

      • 如果你认为你会想要打印一个计数器,你也应该实现 toString()。
      • 在性能方面,Atomic Integer 似乎更快。见gist.github.com/1245597
      • Adamski,while 循环所做的就是提供我们通常从同步中获得的好处。您的解决方案将具有相同的不确定更新时间(从调用者的角度来看)。
      • @sheki:您引用的测试代码仅对访问计数器的单个线程进行了次。 while 循环方法的真正减速是由于线程争用导致几个活动线程最终竞争并因此在 while 循环中旋转浪费 CPU 周期。
      • @CPerkins:使用我的解决方案,如果多个线程尝试同时调用 increment(),我希望它们能够按照在方法调用中阻塞的顺序获得访问权限。我不认为语言规范能保证这一点,但我希望一个明智的 VM 实现会使用某种队列数据结构。但是,根据我之前的评论,我对 while 循环解决方案的主要问题是,您最终可能会遇到许多实时线程旋转并竞争更新计数器,从而浪费 CPU 周期。
      【解决方案6】:

      您可以使用java.util.concurrent.atomic.AtomicInteger 类以原子方式递增。 至于设置上限并回滚到0,您需要在外部进行...也许将所有这些都封装在您自己的包装类中。

      其实貌似可以用compareAndSet查看上限,然后翻车到0

      【讨论】:

        【解决方案7】:

        我必须为自定义 Akka 路由逻辑创建一个类似的循环代码,该逻辑必须不同于默认逻辑以避免网络开销,因为我的逻辑只是选择下一个路由。

        注意:复制自建议的 Java 8 实现:

        import akka.routing.Routee;
        import akka.routing.RoutingLogic;
        import scala.collection.immutable.IndexedSeq;
        
        import java.util.concurrent.atomic.AtomicInteger;
        
        public class CircularRoutingLogic implements RoutingLogic {
        
          final AtomicInteger cycler = new AtomicInteger();
        
          @Override
          public Routee select(Object message, IndexedSeq<Routee> routees) {
            final int size = routees.size();
            return size == 0 ? null : routees.apply(cycler.getAndUpdate(index -> ++index < size ? index : 0));
          }
        }
        

        【讨论】:

          【解决方案8】:

          对于由多个线程并行递增的高强度循环计数器,我建议使用LongAdder(从java 8开始,请参阅Striped64.java中的核心思想),因为它与AtomicLong相比更具可扩展性。很容易适应上述解决方案。

          假设get操作在LongAdder中没有那么频繁。当调用counter.get 时,应用它'counter.get % max_number'。是的,模运算很昂贵,但对于这个用例来说并不常见,它应该摊销总性能成本。

          但请记住,get 操作是非阻塞的,也不是原子的。

          【讨论】:

          • LongAdder 有 get 方法吗?你的意思是总和()? sum() 是的,是非原子的,所以将上限滚动到 0 不会造成问题吗?不管怎样,看看实际的实现很有趣...
          • 我们在这里讨论的是 Java 8,因此您必须考虑 AtomicInteger 如何从 Java 7 更改为 8,基本上 get 和 add 或 add 和 get 是在程序集级别完成的 -@987654331 @- 在这里解释:ashkrit.blogspot.com/2014/02/… LongAdder 是错误的工具,因为每次递增时都需要每个总和。
          • @GuidoMedina,你提到的这个博客有多准确?在 Java 8 中,sun.misc.Unsafe 累积方法仍然调用compareAndSwapInt/Long 方法。也许这些 CAS 方法实际上并没有调用 CAS 处理器指令,但这些是native 方法,所以我们如何确定所有平台;毕竟 XADD 是特定于平台的指令,不是吗?
          • 不是特定于平台的,是特定于 JVM 的,在 JDK 8 之前,它分两个步骤完成,从 JDK 8 开始并转发它是在单个 CPU 原子操作中完成的,谷歌“JDK 8 vs 7 XADD”,你会发现这样的东西:ashkrit.blogspot.com/2014/02/…
          猜你喜欢
          • 2022-06-23
          • 2023-03-10
          • 2019-02-01
          • 1970-01-01
          • 1970-01-01
          • 2018-05-04
          • 1970-01-01
          • 1970-01-01
          • 2015-04-14
          相关资源
          最近更新 更多