我最近开始学习CodaHale / DropWizard指标库。我无法理解Meter类是如何线程安全的(根据文档),特别是mark()和tickIfNecessary()方法:
public void mark(long n) {
tickIfNecessary();
count.add(n);
m1Rate.update(n);
m5Rate.update(n);
m15Rate.update(n);
}
private void tickIfNecessary() {
final long oldTick = lastTick.get();
final long newTick = clock.getTick();
final long age = newTick - oldTick;
if (age > TICK_INTERVAL) {
final long newIntervalStartTick = newTick - age % TICK_INTERVAL;
if (lastTick.compareAndSet(oldTick, newIntervalStartTick)) {
final long requiredTicks = age / TICK_INTERVAL;
for (long i = 0; i < requiredTicks; i++) {
m1Rate.tick();
m5Rate.tick();
m15Rate.tick();
}
}
}
}
我可以看到有一个类型为AtomicLong的lastTick,但仍然可能存在m1-m15速率更长一点的情况,因此另一个线程可以调用这些节拍以及下一个TICK_INTERVAL的一部分。由于费率的tick()方法根本没有同步,因此不会成为竞争条件吗? https://github.com/dropwizard/metrics/blob/3.2-development/metrics-core/src/main/java/com/codahale/metrics/EWMA.java#L86-L95
public void tick() {
final long count = uncounted.sumThenReset();
final double instantRate = count / interval;
if (initialized) {
rate += (alpha * (instantRate - rate));
} else {
rate = instantRate;
initialized = true;
}
}
谢谢,
玛丽安
答案 0 :(得分:1)
据我所知,你是对的。如果tickIfNecessary()
在另一个调用仍在运行时调用age > TICK_INTERVAL
,则可能会从多个线程同时调用m1Rate.tick()
和其他tick()
方法。因此,归结为tick()
,其调用的例程/操作是安全的。
让我们剖析tick()
:
public void tick() {
final long count = uncounted.sumThenReset();
final double instantRate = count / interval;
if (initialized) {
rate += (alpha * (instantRate - rate));
} else {
rate = instantRate;
initialized = true;
}
}
alpha
和interval
仅在实例初始化时设置,并标记为 final 那些线程安全的,因为只读。 count
和instantRate
是本地的,而其他线程无法看到。 rate
和initialized
被标记为 volatile ,并且这些写入应始终对以下读取可见。
如果我没有错,几乎从第一次阅读initialized
到initialized
或rate
的最后一次写入,这对于比赛是开放的,但是有些效果没有当2个线程争夺initialized
到true
的转换时。
似乎大多数有效种族都可以在rate += (alpha * (instantRate - rate));
中发生,特别是丢弃或混合计算,如:
initialized
为true
count
,instantRate
,检查initialized
,第一次读取rate
,我们称之为previous_rate
,无论出于何种原因停止1} LI>
count
,instantRate
,检查initialized
,并计算rate += (alpha * (instantRate - rate));
rate += (alpha * (instantRate - previous_rate));
如果读取和写入以某种方式被排序,以便在所有线程上读取rate
然后写入所有线程,从而有效地丢弃一个或多个计算,则会发生丢弃。
但是这种种族的概率,意味着两个age > TICK_INTERVAL
匹配使得2个线程遇到相同的tick()
方法,特别是rate += (alpha * (instantRate - rate))
可能会非常低并且取决于值不明显。
mark()
方法似乎是线程安全的,只要LongAdderProxy
使用update
/ add
的线程安全数据结构和{{{} 1 {} tick()
中的方法。
我认为只有那些能够回答问题的人才会开放 - 这些比赛没有明显影响或以其他方式减轻 - 是项目作者或对项目的这些部分和计算值有深入了解的人。 / p>
答案 1 :(得分:1)
它是线程安全的,因为来自tickIfNecessary()
的这一行每{?1}}只返回一次
newIntervalStartTick
如果两个线程几乎同时进入if (lastTick.compareAndSet(oldTick, newIntervalStartTick))
会怎样?
两个线程都从tickIfNecessary()
读取相同的值,确定至少oldTick
纳秒已经过去并计算TICK_INTERVAL
。
现在两个线程都尝试newIntervalStartTick
。正如名称lastTick.compareAndSet(oldTick, newIntervalStartTick)
所暗示的,此方法与compareAndSet
到lastTick
的当前值进行比较,并且仅当值等于oldTick
时,它才会被oldTick
原子替换并返回true。
由于这是一个原子指令(在硬件级别!),只有一个线程可以成功。当另一个线程执行此方法时,它会将newIntervalStartTick
视为newIntervalStartTick
的当前值。由于此值不再与lastTick
匹配,因此更新失败,并且该方法返回false,因此此线程不会将oldTick
调用m1Rate.tick()
。
m15Rate.tick()
方法使用EWMA.update(n)
来累积事件计数,从而提供类似的线程安全保证。