编写多线程应用程序时,遇到的最常见问题之一是竞争条件。
我对社区的问题是:
什么是比赛条件?你怎么发现它们?你怎么处理它们?最后,你如何防止它们发生?
当两个或多个线程可以访问共享数据并且他们尝试同时更改它时,会发生竞争条件。因为线程调度算法可以在任何时间在线程之间交换,所以您不知道线程将尝试访问共享数据的顺序。因此,数据变化的结果取决于线程调度算法,即两个线程都 “竞相” 访问 / 改变数据。
当一个线程执行 “check-then-act” 时(例如,“检查”,如果值为 X,然后 “执行” 以执行取决于值为 X 的操作)并且另一个线程对该值执行某些操作时,通常会出现问题在 “检查” 和 “行为” 之间。例如:
if (x == 5) // The "Check"
{
y = x * 2; // The "Act"
// If another thread changed x in between "if (x == 5)" and "y = x * 2" above,
// y will not be equal to 10.
}
关键是,y 可以是 10,或者它可以是任何东西,这取决于另一个线程是否在检查和行为之间改变了 x。你没有真正的认识方式。
为了防止发生竞争条件,您通常会锁定共享数据,以确保一次只能有一个线程访问数据。这意味着这样的事情:
// Obtain lock for x
if (x == 5)
{
y = x * 2; // Now, nothing can change x until the lock is released.
// Therefore y = 10
}
// release lock for x
当访问共享资源的多线程(或其他并行)代码可能以导致意外结果的方式执行时,存在 “竞争条件”。
举个例子:
for ( int i = 0; i < 10000000; i++ )
{
x = x + 1;
}
如果您有 5 个线程同时执行此代码,则 x WOULD NOT 的值最终为 50,000,000。事实上,每次运行都会有所不同。
这是因为,为了使每个线程增加 x 的值,它们必须执行以下操作:(简化,显然)
Retrieve the value of x Add 1 to this value Store this value to x
任何线程都可以随时处于此过程的任何步骤,并且当涉及共享资源时,它们可以相互踩踏。在读取 x 和写回 x 之间的时间内,x 的状态可以被另一个线程改变。
假设一个线程检索 x 的值,但尚未存储它。另一个线程也可以检索相同的 x 值(因为还没有线程改变它),然后它们都将相同的值(x + 1)存储回 x!
例:
Thread 1: reads x, value is 7 Thread 1: add 1 to x, value is now 8 Thread 2: reads x, value is 7 Thread 1: stores 8 in x Thread 2: adds 1 to x, value is now 8 Thread 2: stores 8 in x
通过在访问共享资源的代码之前使用某种锁定机制可以避免竞争条件:
for ( int i = 0; i < 10000000; i++ )
{
//lock x
x = x + 1;
//unlock x
}
在这里,答案每次都是 50,000,000。
有关锁定的更多信息,请搜索:互斥锁,信号量,临界区,共享资源。
什么是比赛条件?
你计划在下午 5 点去看电影。您可以在下午 4 点查询门票的可用性。该代表说他们可以使用。您可以在演出前 5 分钟放松并到达售票窗口。我相信你可以猜到会发生什么:这是一个完整的房子。这里的问题是检查和行动之间的持续时间。你在 4 点询问并在 5 点采取行动。与此同时,其他人抓住了门票。这是一种竞争条件 - 特别是竞争条件下的 “检查然后行动” 情景。
你怎么发现它们?
宗教代码审查,多线程单元测试。没有捷径。这个 Eclipse 插件很少出现,但还没有稳定。
你如何处理和预防他们?
最好的方法是创建无副作用和无状态函数,尽可能使用不可变的函数。但这并非总是可行的。因此,使用 java.util.concurrent.atomic,并发数据结构,正确的同步和基于 actor 的并发性将有所帮助。