匹配条件

数据争用或竞争条件是多线程程序未正确同步时可能发生的问题。如果两个或多个线程在没有同步的情况下访问同一个内存,并且至少有一个访问是写入操作,则会发生数据争用。这导致程序的平台依赖性,可能不一致的行为。例如,计算结果可能取决于线程调度。

读者 - 作家问题

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 ModelJava 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 的计数的正确性更重要。