反向读写锁定

时间:2012-10-23 02:31:11

标签: java multithreading synchronization

通常我们在读取时使用带读锁的ReadWriteLocks,并在写入时写锁。但是我认为反向使用的一个奇特案例可以提供帮助。但希望你们能告诉我一个更好的方法。

这就是我想要的。会有很多写入,但读取量很少。例如,例如,请求延迟的平均计算器。

几乎视为伪代码。

metric.addValue(latency); // Called a lot.

metric.getAverage(); // Called sparingly.

我们可以做到以下几点:

addValue(value) {
  atomicCount.increment();
  atomicSum.increment(value);
}

getAverage() {
  return atomicCount.get() != 0 ? atomicSum.get() / atomicCount.get() : 0.0;
}

问题在于getAverage(),我们“可能”计算一些额外的计数。但大多数情况下可能是正确的值,有时是一个额外的计数。但我只是想让它更精确。

这是诀窍:

ReadWriteLock rw = /* write preference, or a fair lock. */;
Lock read = rw.readLock();
Lock write = rw.writeLock();

addValue(value) {
  read.lock(); // Using read lock when mutating. 
  try { 
    atomicCount.increment();
    atomicSum.increment(value);
  } finally {
    read.unlock();
  }
}

getAverage() {
  write.lock(); // Using write lock when reading.
  try {
    return atomicCount.get() != 0 ? atomicSum.get() / atomicCount.get() : 0.0;
  } finally {
    write.unlock();
  }
}

我的问题是,我可以做得更好吗?

Salt:我知道(强制转换)问题,并且可以避免多次调用count.get()以获得更好的性能,但是不想过多地混淆代码。

5 个答案:

答案 0 :(得分:3)

并发原子增量确实没有意义;他们无论如何也不能同时发生。

最简单的解决方案 - 简单锁定,普通计数/求和变量 - 将表现得更好

lock
    count++;
    sum += value;
unlock

为了更加平行,我们需要“分片” - 每个线程都保持自己的统计数据;读者查询全部图片。 (每个线程的统计数据需要是易变的;读者使用Michael Burr的方法来检索每线程统计数据的稳定版本)

答案 1 :(得分:2)

您可能想要查看以下技术是否表现更好。基本上它通过添加另一个跟踪第一个计数器的计数器来确保计数和总和是“稳定的”,但只有在所有其他值完成更新后才更新,因此不涉及锁定:

addValue(value) {

  while (atomicFlag.get() != 0) {
      // spin
  }
  atomicCount.increment();
  atomicSum.increment(value);
  atomicCount2.increment();
}

getAverage() {
    int count;
    int sum;
    int count2;

    atomicFlag.increment();
    do {
        count = atomicCount.get();
        sum = atomicSum.get();
        count2 = atomicCount2.get();
    } while (count != count2);
    atomicFlag.decrement();

    return count != 0 ? (sum * 1.0) / count : 0.0;
}

答案 2 :(得分:2)

(在这里复制G +的讨论)。

一个优化的想法是使用AtomicLong在Long的不同位置存储值和计数,通过它我们解决了在计算平均值时确保计数和值匹配的问题。

另一个(更大的)优化是使用线程特定度量(前面提到的无法建议)。它具有以下优点。

  • 它可以在写入时避免任何类型的争用。因此,写入时CAS很快,因为没有其他线程写入相同的度量标准。
  • 读取不需要任何锁定。
  • 最重要的是,它会更好地利用L1缓存。

最后一点的解释:

当有多个线程执行大量写操作时从单个共享内存中读取,在多核CPU中,运行在不同内核中的线程只会使其他内核L1缓存失效。因此,必须使用缓存一致性协议从其他核心获取最新值。所有这些都大大减缓了事情的发展。具有线程特定度量可避免此问题。

参考: http://www.cs.washington.edu/education/courses/cse378/07au/lectures/L25-Atomic-Operations.pdf

考虑到这一点,这样的代码表现良好。

private final AtomicLongMap<Long> metric = AtomicLongMap.create();

public void addValue(long value) {
    long threadId = Thread.currentThread().getId();
    metric.addAndGet(threadId, (value << 32) + 1);
}

public synchronized double getAverage() {
    long value = metric.sum();
    int count = (int)value;
    return (count == 0) ? 0 : ((double)(value >> 32))/count;
}

事实上,测试显示它表现最佳 - 比上述无锁解决方案更好!并且也是数量级的。

No thread safety: 3435ms, Average: 1.3532233016178474
(irreputable) Just synchronized {}  4665ms, Average: 4.0
(atuls) reverse read-write lock:    19703ms, Average: 4.0
(michael burr)  17150ms, Average: 4.0
(therealsachin) 1106ms, Average: 4.0

答案 3 :(得分:1)

就正确性而言,我认为你的计划是一个非常狡猾的计划。你已经进行了设置,以便多个更新线程可以独立地增加计数和总数,因此可以安全地通过读锁定。

您的平均计算是在写锁定下进行的,因此可以保证不会更新“读者”,使计数和总数暂时失步。

对我来说,最大的问题是你的方案是否真的能提供简单同步行为的更好性能?虽然你已经通过避免代码中的同步部分删除了读者之间的表面争用点,但是在读者/编写者代码中,读者/编写者代码可能会在同步块中做一些聪明的事情。见ReadWrite Lock documentation。这也警告说,根据实施细节,你的作家可能会遭受饥饿。

只有仔细测量才能告诉我们答案。

答案 4 :(得分:1)

我为每个解决方案运行了一个基准测试,包括我自己的。

只有来自100个线程的addValue,每个循环包含100个任务,循环,每个任务有10000个更新,值为0到9999.结果是:

(irreputable) Just synchronized {}: 7756 ms  Average: 4999.5
(atuls) My reverse read-write lock: 16523 ms Average: 4999.5
(michael burr) Double counter trick: 10698 Average: 4999.5
No thread safety: 4115 ms Average: 4685.0
(atuls) Not thread safe v1. 11189 ms Average: 4999.5

看起来无可争辩是正确的:)