匹配条件
数据争用或竞争条件是多线程程序未正确同步时可能发生的问题。如果两个或多个线程在没有同步的情况下访问同一个内存,并且至少有一个访问是写入操作,则会发生数据争用。这导致程序的平台依赖性,可能不一致的行为。例如,计算结果可能取决于线程调度。
writer_thread {
write_to(buffer)
}
reader_thread {
read_from(buffer)
}
简单的解决方案:
writer_thread {
lock(buffer)
write_to(buffer)
unlock(buffer)
}
reader_thread {
lock(buffer)
read_from(buffer)
unlock(buffer)
}
如果只有一个读取器线程,这个简单的解决方案很有效,但如果有多个读取器线程,则会不必要地减慢执行速度,因为读取器线程可以同时读取。
避免此问题的解决方案可能是:
writer_thread {
lock(reader_count)
if(reader_count == 0) {
write_to(buffer)
}
unlock(reader_count)
}
reader_thread {
lock(reader_count)
reader_count = reader_count + 1
unlock(reader_count)
read_from(buffer)
lock(reader_count)
reader_count = reader_count - 1
unlock(reader_count)
}
请注意,reader_count
在整个写入操作中被锁定,因此在写入尚未完成时,没有读者可以开始阅读。
现在许多读者可以同时阅读,但可能会出现一个新问题:reader_count
可能永远不会到达 0
,这样编写器线程永远不能写入缓冲区。这被称为饥饿 ,有不同的解决方案来避免它。
即使是看似正确的程序也可能存在问题:
boolean_variable = false
writer_thread {
boolean_variable = true
}
reader_thread {
while_not(boolean_variable)
{
do_something()
}
}
示例程序可能永远不会终止,因为读者线程可能永远不会看到来自编写器线程的更新。例如,如果硬件使用 CPU 缓存,则可以缓存这些值。并且由于对正常字段的写入或读取不会导致刷新缓存,因此读取线程可能永远不会看到更改的值。
C++和 Java 在所谓的内存模型中定义了正确同步的含义: C++ Memory Model , Java Memory Model 。
在 Java 中,解决方案是将字段声明为 volatile:
volatile boolean boolean_field;
在 C++中,解决方案是将字段声明为原子:
std::atomic<bool> data_ready(false)
数据竞争是一种竞争条件。但并非所有竞争条件都是数据竞赛。由多个线程调用的以下内容会导致竞争条件,但不会导致数据争用:
class Counter {
private volatile int count = 0;
public void addOne() {
i++;
}
}
它根据 Java 内存模型规范正确同步,因此它不是数据竞争。但它仍会导致竞争条件,例如结果取决于线程的交错。
并非所有数据竞争都是错误。所谓的良性竞争条件的一个例子是 sun.reflect.NativeMethodAccessorImpl:
class NativeMethodAccessorImpl extends MethodAccessorImpl {
private Method method;
private DelegatingMethodAccessorImpl parent;
private int numInvocations;
NativeMethodAccessorImpl(Method method) {
this.method = method;
}
public Object invoke(Object obj, Object[] args)
throws IllegalArgumentException, InvocationTargetException
{
if (++numInvocations > ReflectionFactory.inflationThreshold()) {
MethodAccessorImpl acc = (MethodAccessorImpl)
new MethodAccessorGenerator().
generateMethod(method.getDeclaringClass(),
method.getName(),
method.getParameterTypes(),
method.getReturnType(),
method.getExceptionTypes(),
method.getModifiers());
parent.setDelegate(acc);
}
return invoke0(method, obj, args);
}
...
}
这里代码的性能比 numInvocation 的计数的正确性更重要。