这个类是线程安全的吗?
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();
}
}
你觉得怎么样?
是的,前提是你把地图定为最终版。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 ms
懒惰计数器:207 ms
MPCounter: 303 ms
更好计数器:135 ms
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();
}
}
}
编辑
是的,如果您将地图设为final
.否则,不能保证所有线程在首次调用 add()
时都能看到最新版本的映射数据结构。
几根线可以到达if()
体。putIfAbsent()
将确保地图中仅放置一个AtomicLong
。
如果没有新值在映射中,putIfAbsent()
就无法返回。
因此,当执行第二个get()
时,它永远不会获得null
值,并且由于只能将单个AtomicLong
添加到映射中,因此所有线程都将获得相同的实例。
[编辑2] 下一个问题:效率如何?
此代码更快,因为它避免了不必要的搜索:
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,它有一个在找不到键时调用的方法。这样,地图只需搜索一次,我就不必创建额外的实例。
似乎没有人有完整的解决方案,即:
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();
}
这个呢:
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();
}
}
- 应
final
map
,以确保在调用第一个方法之前,它对所有线程完全可见。(有关详细信息,请参阅 17.5 最终字段语义(Java 语言规范( - 我认为
if
是多余的,我希望我没有监督任何事情。
编辑:添加了Java语言规范中的引用:
这个解决方案(请注意,我只显示add
方法的主体 - 其余的保持不变!(免除了对get
的任何调用:
final AtomicLong newVal = new AtomicLong(),
prevVal = map.putIfAbsent(name, newVal);
return (prevVal != null? prevVal : newVal).incrementAndGet();
额外的get
很可能比额外的new AtomicLong()
贵得多。
我认为你最好这样:
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 返回的值(。
我假设您永远不会重新创建地图。