5

代码片段 - 1

class RequestObject implements Runnable
{
    private static Integer nRequests = 0;

    @Override
    public void run()
    {       
        synchronized (nRequests)
        {
            nRequests++;
        }
    }
}

代码片段 - 2

public class Racer implements Runnable
{
    public static Boolean won = false;    

    @Override
    public void run()
    {
        synchronized (won)
        {
            if (!won)
            won = true;
        }
    }        
}

我遇到了第一个代码片段的竞争条件。我知道这是因为我获得了一个不可变对象(整数类型)的锁定。

我编写了第二个代码片段,它再次不受“布尔”不可变的影响。但这有效(输出运行中不显示竞争条件)。如果我正确理解了上一个问题的解决方案,那么以下是可能出错的一种可能方式

  1. 线程 1 获得了指向的对象(比如 A)的锁won
  2. 线程 2 现在尝试获取指向的对象的锁,won并进入 A 的等待队列
  3. 线程 1 进入同步块,验证 A 是否为假,并通过说won = true(A 认为它赢得了比赛)来创建一个新对象(比如 B)。
  4. 'won' 现在指向 B。线程 1 释放对象 A 上的锁(不再由 指向won
  5. 现在,在对象 A 的等待队列中的线程 2 被唤醒并在对象 A 上获得锁,该对象 A 仍然false(不变)。它现在进入同步块并假设它也赢了,这是不正确的。

为什么第二个代码片段一直工作正常?

4

4 回答 4

8
    synchronized (won)
    {
        if (!won)
        won = true;
    }

run在这里,您有一个暂时的竞争条件,您没有注意到它,因为它在第一次执行该方法后就消失了。之后,won变量不断指向表示的同一个实例Booleantrue因此它可以正确地用作互斥锁。

这并不是说您应该在实际项目中编写此类代码。所有锁对象都应该分配给final变量,以确保它们永远不会改变。

于 2013-08-04T09:38:03.340 回答
2

我遇到了第一个代码片段的竞争条件。我知道这是因为我获得了一个不可变对象(整数类型)的锁定。

其实,根本不是这个原因。获得对不可变对象的锁定将“工作”得很好。问题是它可能不会做任何有用的事情......

第一个示例中断的真正原因是您锁定了错误的东西。当你执行这个 - nRequests++- 你实际上在做什么相当于这个非原子序列:

    int temp = nRequests.integerValue();
    temp = temp + 1;
    nRequests = Integer.valueOf(temp);

换句话说,您正在分配一个不同的对象引用static变量nRequests

问题是,在您的代码片段中,每次对变量进行更新时,线程都会在不同的对象上同步。这是因为每个线程都会更改对要锁定的对象的引用。

为了正确同步,所有线程都需要锁定同一个对象;例如

class RequestObject implements Runnable
{
    private static Integer nRequests = 0;
    private static final Object lock = new Object();

    @Override
    public void run()
    {       
        synchronized (lock)
        {
            nRequests++;
        }
    }
}

事实上,第二个例子和第一个例子有同样的问题。您在测试中没有注意到它的原因是从won == false到的转换won == true只发生了一次......所以潜在的竞争条件实际上最终发生的可能性要小得多。

于 2013-08-05T03:22:52.193 回答
2

synchronized一个对象是否不可变与它是否适合作为语句中的锁对象无关。然而,重要的是,进入同一组关键区域的所有线程都使用相同的对象(因此使对象引用可能是明智的final),但可以修改对象本身而不影响它的“锁定性”。此外,两个(或更多)不同的synchronized语句可以使用不同的引用变量并且仍然是互斥的,只要不同的引用变量都引用同一个对象。

在上面的示例中,临界区中的代码将一个对象替换为另一个对象,这是一个问题。锁在object上,而不是在reference上,所以更改 object 是禁忌。

于 2013-08-05T03:46:21.383 回答
0

实际上,您的第二个代码也不是线程安全的。请使用下面的代码自行检查(您会发现第一个 print 语句有时会是 2,这意味着同步块内有两个线程!)。底线:代码片段 - 1 和代码片段 - 2 基本相同,因此不是线程安全的......

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

public class Racer implements Runnable {
    public static AtomicInteger counter = new AtomicInteger(0);
    public static Boolean won = false;    

    @Override
    public void run() {
        synchronized (won) {
            System.out.println(counter.incrementAndGet()); //should be always 1; otherwise race condition
            if (!won) {
                won = true;
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(counter.decrementAndGet()); //should be always 0; otherwise race condition
        }
    }   

    public static void main(String[] args) {
        int numberOfThreads = 20;
        ExecutorService executor = Executors.newFixedThreadPool(numberOfThreads);

        for(int i = 0; i < numberOfThreads; i++) {
            executor.execute(new Racer());
        }

        executor.shutdown();
    }
}
于 2013-08-04T10:01:29.917 回答