数据竞争或竞争条件是多线程程序未正确同步时可能发生的问题。如果两个或多个线程在没有同步的情况下访问同一内存,并且至少其中一个访问是“写”操作,则会发生数据竞争。这会导致程序的平台相关的、可能不一致的行为。例如,计算结果可能取决于线程调度。
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++内存模型,Java内存模型。
在 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的计数的正确性更重要。