在编写多线程应用程序时,遇到的最常见的问题之一是竞争条件。

我对社区的问题是:

竞态条件是什么? 你如何发现它们? 你是如何处理的? 最后,你如何防止它们的发生?


当前回答

什么是竞态条件?

You are planning to go to a movie at 5 pm. You inquire about the availability of the tickets at 4 pm. The representative says that they are available. You relax and reach the ticket window 5 minutes before the show. I'm sure you can guess what happens: it's a full house. The problem here was in the duration between the check and the action. You inquired at 4 and acted at 5. In the meantime, someone else grabbed the tickets. That's a race condition - specifically a "check-then-act" scenario of race conditions.

你如何发现它们?

宗教代码审查多线程单元测试没有捷径。在此基础上出现了一些Eclipse插件,但还没有稳定的插件。

你如何处理和预防它们?

最好的方法是创建无副作用和无状态的函数,尽可能使用不可变函数。但这并不总是可能的。使用java。util。concurrent。原子的、并发的数据结构、适当的同步和基于参与者的并发将有所帮助。

并发性的最佳资源是JCIP。你也可以在这里得到更多关于上述解释的细节。

其他回答

在竞争条件和数据竞争之间有一个重要的技术差异。大多数答案似乎假设这些术语是等价的,但事实并非如此。

当两条指令访问相同的内存位置时,就会发生数据竞争,其中至少有一个是写,并且在这些访问之间没有发生排序之前。现在,什么构成happens before顺序还存在很多争论,但一般来说,同一个锁变量上的ulock-lock对和同一个条件变量上的wait-signal对诱导的是happens before顺序。

竞态条件是语义错误。它是发生在事件的时间或顺序上的缺陷,会导致错误的程序行为。

许多竞态条件可能(事实上也是)是由数据竞态引起的,但这是不必要的。事实上,数据竞争和竞争条件既不是彼此的必要条件,也不是彼此的充分条件。这篇博客文章也用一个简单的银行交易例子很好地解释了两者的区别。下面是另一个简单的例子来解释这种区别。

既然我们已经确定了术语,让我们试着回答最初的问题。

由于竞争条件是语义错误,因此没有检测它们的通用方法。这是因为在一般情况下,不可能有一个自动的oracle来区分正确和不正确的程序行为。种族检测是一个无法确定的问题。

另一方面,数据竞争有一个精确的定义,它不一定与正确性有关,因此可以检测到它们。数据竞争检测器有很多种(静态/动态数据竞争检测、基于锁集的数据竞争检测、基于先于事件发生的数据竞争检测、混合数据竞争检测)。最先进的动态数据竞争检测器是ThreadSanitizer,它在实践中工作得非常好。

处理数据竞争通常需要一些编程规程来在访问共享数据之间的边之前诱导happens(在开发过程中,或者在使用上述工具检测到它们之后)。这可以通过锁、条件变量、信号量等来实现。但是,还可以使用不同的编程范例,例如消息传递(而不是共享内存),以避免构造造成的数据竞争。

您并不总是希望丢弃竞态条件。如果你有一个可以被多个线程读写的标志,并且这个标志被一个线程设置为“done”,这样当标志被设置为“done”时,其他线程就会停止处理,你不希望这个“竞争条件”被消除。事实上,这可以被称为良性竞态条件。

然而,使用检测竞态条件的工具,它将被视为有害的竞态条件。

更多关于比赛情况的详细信息,请访问http://msdn.microsoft.com/en-us/magazine/cc546569.aspx。

竞态条件是一种bug,只会在特定的时间条件下发生。

例子: 假设您有两个线程,A和B。

在线程A中:

if( object.a != 0 )
    object.avg = total / object.a

线程B:

object.a = 0

如果线程A在检查完对象后被抢占。a不为空,B将执行a = 0,当线程a将获得处理器时,它将执行“除零”。

这个错误只发生在if语句之后的线程A被抢占时,这是非常罕见的,但它是有可能发生的。

一个有点规范的定义是“当两个线程同时访问内存中的同一个位置,并且至少有一次访问是写操作。”在这种情况下,“reader”线程可能获得旧值或新值,这取决于哪个线程“赢得了比赛”。这并不总是一个bug——事实上,一些非常复杂的低级算法会故意这样做——但通常应该避免。@Steve Gury的例子很好地说明了这可能是个问题。

为了更好地理解竞态条件,请尝试以下基本示例:

    public class ThreadRaceCondition {

    /**
     * @param args
     * @throws InterruptedException
     */
    public static void main(String[] args) throws InterruptedException {
        Account myAccount = new Account(22222222);

        // Expected deposit: 250
        for (int i = 0; i < 50; i++) {
            Transaction t = new Transaction(myAccount,
                    Transaction.TransactionType.DEPOSIT, 5.00);
            t.start();
        }

        // Expected withdrawal: 50
        for (int i = 0; i < 50; i++) {
            Transaction t = new Transaction(myAccount,
                    Transaction.TransactionType.WITHDRAW, 1.00);
            t.start();

        }

        // Temporary sleep to ensure all threads are completed. Don't use in
        // realworld :-)
        Thread.sleep(1000);
        // Expected account balance is 200
        System.out.println("Final Account Balance: "
                + myAccount.getAccountBalance());

    }

}

class Transaction extends Thread {

    public static enum TransactionType {
        DEPOSIT(1), WITHDRAW(2);

        private int value;

        private TransactionType(int value) {
            this.value = value;
        }

        public int getValue() {
            return value;
        }
    };

    private TransactionType transactionType;
    private Account account;
    private double amount;

    /*
     * If transactionType == 1, deposit else if transactionType == 2 withdraw
     */
    public Transaction(Account account, TransactionType transactionType,
            double amount) {
        this.transactionType = transactionType;
        this.account = account;
        this.amount = amount;
    }

    public void run() {
        switch (this.transactionType) {
        case DEPOSIT:
            deposit();
            printBalance();
            break;
        case WITHDRAW:
            withdraw();
            printBalance();
            break;
        default:
            System.out.println("NOT A VALID TRANSACTION");
        }
        ;
    }

    public void deposit() {
        this.account.deposit(this.amount);
    }

    public void withdraw() {
        this.account.withdraw(amount);
    }

    public void printBalance() {
        System.out.println(Thread.currentThread().getName()
                + " : TransactionType: " + this.transactionType + ", Amount: "
                + this.amount);
        System.out.println("Account Balance: "
                + this.account.getAccountBalance());
    }
}

class Account {
    private int accountNumber;
    private double accountBalance;

    public int getAccountNumber() {
        return accountNumber;
    }

    public double getAccountBalance() {
        return accountBalance;
    }

    public Account(int accountNumber) {
        this.accountNumber = accountNumber;
    }

    // If this method is not synchronized, you will see race condition on
    // Remove syncronized keyword to see race condition
    public synchronized boolean deposit(double amount) {
        if (amount < 0) {
            return false;
        } else {
            accountBalance = accountBalance + amount;
            return true;
        }
    }

    // If this method is not synchronized, you will see race condition on
    // Remove syncronized keyword to see race condition
    public synchronized boolean withdraw(double amount) {
        if (amount > accountBalance) {
            return false;
        } else {
            accountBalance = accountBalance - amount;
            return true;
        }
    }
}