非同步读取(结合同步写入)是否最终一致
Are unsynchronized reads (combined with synchronized writes) eventually consistent
我有一个包含许多编写器线程和一个 reader 线程的用例。正在写入的数据是一个事件计数器,正在由显示线程读取。
计数器只会增加并且显示是供人类使用的,因此确切的时间点值并不重要。为此,我认为一个解决方案是正确的,只要:
- reader 线程看到的值永远不会减少。
- 读取最终是一致的。在一定时间没有任何写入后,所有读取都将 return 精确值。
假设编写器彼此正确同步,是否有必要将 reader 线程与编写器同步以保证正确性,如上定义?
一个简化的例子。如上定义,这是否正确?
public class Eventual {
private static class Counter {
private int count = 0;
private Lock writeLock = new ReentrantLock();
// Unsynchronized reads
public int getCount() {
return count;
}
// Synchronized writes
public void increment() {
writeLock.lock();
try {
count++;
} finally {
writeLock.unlock();
}
}
}
public static void main(String[] args) {
List<Thread> contentiousThreads = new ArrayList<>();
final Counter sharedCounter = new Counter();
// 5 synchronized writer threads
for(int i = 0; i < 5; ++i) {
contentiousThreads.add(new Thread(new Runnable(){
@Override
public void run() {
for(int i = 0; i < 20_000; ++i) {
sharedCounter.increment();
safeSleep(1);
}
}
}));
}
// 1 unsynchronized reader thread
contentiousThreads.add(new Thread(new Runnable(){
@Override
public void run() {
for(int i = 0; i < 30; ++i) {
// This value should:
// +Never decrease
// +Reach 100,000 if we are eventually consistent.
System.out.println("Count: " + sharedCounter.getCount());
safeSleep(1000);
}
}
}));
contentiousThreads.stream().forEach(t -> t.start());
// Just cleaning up...
// For the question, assume readers/writers run indefinitely
try {
for(Thread t : contentiousThreads) {
t.join();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private static void safeSleep(int ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
//Don't care about error handling for now.
}
}
}
急着看错了问题,我误以为 reader 线程是主线程。
由于the happens-before relationship:
主线程可以正确显示最终计数
All actions in a thread happen-before any other thread successfully returns from a join on that thread.
一旦主线程加入了所有写入器,计数器的更新值就可见了。但是,没有强制 reader 的视图更新的发生前关系,您将受制于 JVM 实现。如果经过足够长的时间,JLS 中没有关于值变得可见的承诺,它留待实现。计数器值可能会被缓存,并且 reader 可能看不到任何更新。
在一个平台上测试这个并不能保证其他平台会做什么,所以不要因为测试在您的 PC 上通过就认为这没问题。我们中有多少人在部署到的同一平台上进行开发?
在计数器上使用 volatile
或使用 AtomicInteger 将是很好的修复方法。使用 AtomicInteger 将允许从编写器线程中删除锁。只有在只有一个编写器的情况下才可以在没有锁定的情况下使用 volatile,当存在两个或多个编写器时,++ 或 += 不是线程安全的将是一个问题。使用 Atomic class 是更好的选择。
(顺便说一下,吃掉 InterruptedException 不是 "safe",它只是让线程对中断没有响应,当您的程序要求线程提前完成时会发生这种情况。)
无法保证 reader 会看到计数更新。一个简单的解决方法是使计数不稳定。
如另一个答案所述,在您当前的示例中,"Final Count" 将是正确的,因为主线程正在加入编写器线程(因此建立了先行关系)。但是,您的 reader 线程永远无法保证看到计数的任何更新。
我有一个包含许多编写器线程和一个 reader 线程的用例。正在写入的数据是一个事件计数器,正在由显示线程读取。
计数器只会增加并且显示是供人类使用的,因此确切的时间点值并不重要。为此,我认为一个解决方案是正确的,只要:
- reader 线程看到的值永远不会减少。
- 读取最终是一致的。在一定时间没有任何写入后,所有读取都将 return 精确值。
假设编写器彼此正确同步,是否有必要将 reader 线程与编写器同步以保证正确性,如上定义?
一个简化的例子。如上定义,这是否正确?
public class Eventual {
private static class Counter {
private int count = 0;
private Lock writeLock = new ReentrantLock();
// Unsynchronized reads
public int getCount() {
return count;
}
// Synchronized writes
public void increment() {
writeLock.lock();
try {
count++;
} finally {
writeLock.unlock();
}
}
}
public static void main(String[] args) {
List<Thread> contentiousThreads = new ArrayList<>();
final Counter sharedCounter = new Counter();
// 5 synchronized writer threads
for(int i = 0; i < 5; ++i) {
contentiousThreads.add(new Thread(new Runnable(){
@Override
public void run() {
for(int i = 0; i < 20_000; ++i) {
sharedCounter.increment();
safeSleep(1);
}
}
}));
}
// 1 unsynchronized reader thread
contentiousThreads.add(new Thread(new Runnable(){
@Override
public void run() {
for(int i = 0; i < 30; ++i) {
// This value should:
// +Never decrease
// +Reach 100,000 if we are eventually consistent.
System.out.println("Count: " + sharedCounter.getCount());
safeSleep(1000);
}
}
}));
contentiousThreads.stream().forEach(t -> t.start());
// Just cleaning up...
// For the question, assume readers/writers run indefinitely
try {
for(Thread t : contentiousThreads) {
t.join();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
private static void safeSleep(int ms) {
try {
Thread.sleep(ms);
} catch (InterruptedException e) {
//Don't care about error handling for now.
}
}
}
由于the happens-before relationship:
主线程可以正确显示最终计数All actions in a thread happen-before any other thread successfully returns from a join on that thread.
一旦主线程加入了所有写入器,计数器的更新值就可见了。但是,没有强制 reader 的视图更新的发生前关系,您将受制于 JVM 实现。如果经过足够长的时间,JLS 中没有关于值变得可见的承诺,它留待实现。计数器值可能会被缓存,并且 reader 可能看不到任何更新。
在一个平台上测试这个并不能保证其他平台会做什么,所以不要因为测试在您的 PC 上通过就认为这没问题。我们中有多少人在部署到的同一平台上进行开发?
在计数器上使用 volatile
或使用 AtomicInteger 将是很好的修复方法。使用 AtomicInteger 将允许从编写器线程中删除锁。只有在只有一个编写器的情况下才可以在没有锁定的情况下使用 volatile,当存在两个或多个编写器时,++ 或 += 不是线程安全的将是一个问题。使用 Atomic class 是更好的选择。
(顺便说一下,吃掉 InterruptedException 不是 "safe",它只是让线程对中断没有响应,当您的程序要求线程提前完成时会发生这种情况。)
无法保证 reader 会看到计数更新。一个简单的解决方法是使计数不稳定。
如另一个答案所述,在您当前的示例中,"Final Count" 将是正确的,因为主线程正在加入编写器线程(因此建立了先行关系)。但是,您的 reader 线程永远无法保证看到计数的任何更新。