我有一个包含许多编写器线程和单个读者线程的用例。正在写入的数据是由显示线程读取的事件计数器。
计数器只会增加并且显示器适用于人类,因此准确的时间点值并不重要。为此,我认为解决方案只要正确:
假设编写者彼此正确同步,是否有必要将读者线程与编写者同步以保证正确性,如上所述?
一个简化的例子。如上所述,这是否正确?
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.
}
}
}
答案 0 :(得分:3)
无法保证读者会看到计数更新。一个简单的解决方法是使计数不稳定。
如另一个答案中所述,在您当前的示例中,“最终计数”将是正确的,因为主线程正在加入编写器线程(从而建立发生在之前的关系)。但是,您的读者线程永远不能保证看到计数的任何更新。
答案 1 :(得分:1)
JTahlborn is correct, +1 from me.我匆匆忙忙地误读了这个问题,我误以为读者线程是主线。
由于the happens-before relationship:
,主线程可以正确显示最终计数线程中的所有操作都会发生 - 在任何其他线程从该线程的连接成功返回之前。
一旦主线程连接到所有编写器,则计数器的更新值可见。然而,没有发生在之前的关系强迫读者的观点得到更新,你受JVM实现的支配。如果有足够的时间过去,JLS中没有关于值可见的承诺,它对实现保持开放。计数器值可能会被缓存,读者可能无法看到任何更新。
在一个平台上进行测试并不能保证其他平台会做什么,所以不要因为测试在你的PC上传递而认为这没关系。我们中有多少人在我们部署的同一平台上开发?
在柜台上使用volatile
或使用AtomicInteger将是很好的修复。使用AtomicInteger将允许从编写器线程中删除锁。使用volatile而不锁定只有在只有一个编写器的情况下才会出现,当存在两个或多个编写器时,++或+ =不是线程安全将是一个问题。使用Atomic类是更好的选择。
(顺便说一句,吃掉InterruptedException并不是“安全”,它只是让线程无法响应中断,当你的程序要求线程提前完成时就会发生这种情况。)