3

这个类是线程安全的吗?

class Counter {
  private ConcurrentMap<String, AtomicLong> map = 
    new ConcurrentHashMap<String, AtomicLong>();
  public long add(String name) {
    if (this.map.get(name) == null) {
      this.map.putIfAbsent(name, new AtomicLong());
    }
    return this.map.get(name).incrementAndGet();
  }
}

你怎么看?

4

6 回答 6

6

是的,只要你把地图做成最终版。if 不是必需的,但如果需要,您可以出于性能原因保留它,尽管它很可能不会产生明显的差异:

public long add(String name) {
  this.map.putIfAbsent(name, new AtomicLong());
  return this.map.get(name).incrementAndGet();
}

编辑

为此,我已经快速测试了这两种实现(有和没有检查)。对同一字符串的 1000 万次调用需要:

  • 250 毫秒检查
  • 480 毫秒无检查

这证实了我所说的:除非您调用此方法数百万次,或者它是代码的性能关键部分,否则它不会产生任何影响。

编辑 2

完整的测试结果 - 看看BetterCounter哪个会产生更好的结果。现在测试非常具体(没有争用 + get 总是有效),不一定符合您的使用情况。

计数器:482 毫秒
LazyCounter:207 毫秒
MPCounter:303 毫秒
BetterCounter:135 毫秒

public class Test {

    public static void main(String args[]) throws IOException {
        Counter count = new Counter();
        LazyCounter lazyCount = new LazyCounter();
        MPCounter mpCount = new MPCounter();
        BetterCounter betterCount = new BetterCounter();

        //WARM UP
        for (int i = 0; i < 10_000_000; i++) {
            count.add("abc");
            lazyCount.add("abc");
            mpCount.add("abc");
            betterCount.add("abc");
        }

        //TEST
        long start = System.nanoTime();
        for (int i = 0; i < 10_000_000; i++) {
            count.add("abc");
        }
        long end = System.nanoTime();
        System.out.println((end - start) / 1000000);

        start = System.nanoTime();
        for (int i = 0; i < 10_000_000; i++) {
            lazyCount.add("abc");
        }
        end = System.nanoTime();
        System.out.println((end - start) / 1000000);

        start = System.nanoTime();
        for (int i = 0; i < 10_000_000; i++) {
            mpCount.add("abc");
        }
        end = System.nanoTime();
        System.out.println((end - start) / 1000000);

        start = System.nanoTime();
        for (int i = 0; i < 10_000_000; i++) {
            betterCount.add("abc");
        }
        end = System.nanoTime();
        System.out.println((end - start) / 1000000);        
    }

    static class Counter {

        private final ConcurrentMap<String, AtomicLong> map =
                new ConcurrentHashMap<String, AtomicLong>();

        public long add(String name) {
            this.map.putIfAbsent(name, new AtomicLong());
            return this.map.get(name).incrementAndGet();
        }
    }

    static class LazyCounter {

        private final ConcurrentMap<String, AtomicLong> map =
                new ConcurrentHashMap<String, AtomicLong>();

        public long add(String name) {
            if (this.map.get(name) == null) {
                this.map.putIfAbsent(name, new AtomicLong());
            }
            return this.map.get(name).incrementAndGet();
        }
    }

    static class BetterCounter {

        private final ConcurrentMap<String, AtomicLong> map =
                new ConcurrentHashMap<String, AtomicLong>();

            public long add(String name) {
                AtomicLong counter = this.map.get(name);
                if (counter != null)
                    return counter.incrementAndGet();

                AtomicLong newCounter = new AtomicLong();
                counter = this.map.putIfAbsent(name, newCounter);

                return (counter == null ? newCounter.incrementAndGet() : counter.incrementAndGet());
            }
    }

    static class MPCounter {

        private final ConcurrentMap<String, AtomicLong> map =
                new ConcurrentHashMap<String, AtomicLong>();

        public long add(String name) {
            final AtomicLong newVal = new AtomicLong(),
                    prevVal = map.putIfAbsent(name, newVal);
            return (prevVal != null ? prevVal : newVal).incrementAndGet();
        }
    }
}
于 2012-05-08T12:12:37.117 回答
2

编辑

是的,如果你制作地图finaladd()否则,不能保证所有线程在第一次调用时都能看到最新版本的地图数据结构。

几根线可以到达的身体if()。这putIfAbsent()将确保只有一个AtomicLong被放入地图。

putIfAbsent()如果地图中没有新值,就不应该有任何方法可以返回。

所以当第二个get()被执行时,它永远不会得到一个null值,因为只有一个AtomicLong可以被添加到映射中,所有线程都将获得相同的实例。

[EDIT2]下一个问题:效率如何?

此代码更快,因为它避免了不必要的搜索:

public long add(String name) {
    AtomicLong counter = map.get( name );
    if( null == counter ) {
        map.putIfAbsent( name, new AtomicLong() );
        counter = map.get( name ); // Have to get again!!!
    }
    return counter.incrementAndGet();
}

这就是为什么我更喜欢 Google 的CacheBuilder的原因,它有一个在找不到密钥时调用的方法。这样,地图只被搜索一次我不必创建额外的实例。

于 2012-05-08T12:11:29.067 回答
1

似乎没有人拥有完整的解决方案,即:

  public long add(String name) {
    AtomicLong counter = this.map.get(name);
    if (counter == null) {
      AtomicLong newCounter = new AtomicLong();
      counter = this.map.putIfAbsent(name, newCounter);
      if(counter == null) {
        counter = newCounter;
      }
    }

    return counter.incrementAndGet();
  }
于 2012-05-08T13:05:15.140 回答
0

那这个呢:

class Counter {

  private final ConcurrentMap<String, AtomicLong> map = 
    new ConcurrentHashMap<String, AtomicLong>();

  public long add(String name) {
    this.map.putIfAbsent(name, new AtomicLong());
    return this.map.get(name).incrementAndGet();
  }
}

编辑:添加了来自 Java 语言规范的引用:

于 2012-05-08T12:13:20.017 回答
0

我认为您最好使用以下方法:

class Counter { 
  private ConcurrentMap<String, AtomicLong> map = new ConcurrentHashMap<String, AtomicLong>();

  public long add(String name) {
    AtomicLong counter = this.map.get(name);
    if (counter == null) {
      AtomicLong newCounter = new AtomicLong();
      counter = this.map.putIfAbsent(name, newCounter);
      if (counter == null) {
        // The new counter was added - use it
        counter = newCounter;
      }
    }

    return counter.incrementAndGet();
  }
}

否则可能会同时添加多个线程而您不会注意到(因为您忽略了 putIfAbsent 返回的值)。

我假设您永远不会重新创建地图。

于 2012-05-08T12:17:32.993 回答
0

此解决方案(请注意,我只显示add方法的主体 - 其余部分保持不变!)使您无需调用get

final AtomicLong newVal = new AtomicLong(), 
                 prevVal = map.putIfAbsent(name, newVal);
return (prevVal != null? prevVal : newVal).incrementAndGet();

很可能一个 extraget比一个 extra 贵得多new AtomicLong()

于 2012-05-08T12:44:02.767 回答