它是一种线程安全的机制吗?

时间:2012-05-08 12:09:06

标签: java multithreading thread-safety

这个类是否是线程安全的?

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();
  }
}

您怎么看?

6 个答案:

答案 0 :(得分:6)

是的,只要你让地图最终完成。 if是没有必要的,但如果你愿意,你可以出于性能原因保留它,尽管它很可能没有明显的区别:

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

编辑

为此,我已经快速测试了两种实现方式(有和没有检查)。对同一个字符串进行10百万次调用:

  • 检查时间为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();
        }
    }
}

答案 1 :(得分:2)

修改

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

多个线程可以到达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,它有一个无法找到密钥时调用的方法。这样,地图只会被搜索一次我不需要创建额外的实例。

答案 2 :(得分: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();
  }

答案 3 :(得分: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语言规范的引用:

答案 4 :(得分: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返回的值)。

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

答案 5 :(得分:0)

此解决方案(请注意,我只显示add方法的正文 - 其余方法保持不变!)让您免除对get的任何调用:

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

很可能额外的get比额外new AtomicLong()更昂贵。