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