不可变对象的同步(在java中)

时间:2013-08-04 09:33:06

标签: java multithreading synchronization

代码段 - 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;
        }
    }        
}

我在第一个代码片段中遇到了竞争条件。我understood这是因为我在一个不可变对象(Integer类型)上获得了一个锁。

我已经编写了第二个代码片段,它再次不受“布尔”不可变的影响。但是这样可行(输出运行中不显示竞争条件)。如果我已正确理解previous question的解决方案,则以下是可能出现问题的方法

  1. 线程1获取won
  2. 指向的对象(例如A)的锁定
  3. 线程2现在试图锁定won指向的对象并进入A的等待队列
  4. 线程1进入同步块,验证A是否为假并通过说won = true(A认为它赢得了比赛)创建了一个新对象(比如说B)。
  5. 'won'现在指向B.线程1释放对象A上的锁(won不再指向)
  6. 现在,在对象A的等待队列中的thread-2被唤醒并获得对象A的锁定,该对象仍然是false(不可变的)。它现在进入同步块并假设它也赢了,这是不正确的。
  7. 为什么第二个代码段始终正常工作?

4 个答案:

答案 0 :(得分:7)

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

这里有一个瞬态竞争条件,你没有注意到它,因为它在第一次执行run方法后消失了。之后,won变量始终指向代表Boolean的{​​{1}}的同一个实例,因此可以正确地用作互斥锁。

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

答案 1 :(得分:2)

  

我在第一个代码片段中遇到了竞争条件。我明白这是因为我在一个不可变对象(Integer类型)上获得了一个锁。

实际上,这根本不是原因。获取不可变对象的锁定将“正常”工作。问题是它可能不会做任何有用的事情......

第一个例子打破的真正原因是你正在锁定错误的东西。执行此操作时 - nRequests++ - 您实际执行的操作等同于此非原子序列:

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

换句话说,您要为static变量nRequests分配不同的对象引用。

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

为了正确同步,所有线程都需要锁定同一个对象; e.g。

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

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

事实上,第二个例子遇到的问题与第一个问题相同。您在测试中没有注意到的原因是从won == falsewon == true的转换只发生一次......所以潜在竞争条件实际发生的可能性要小得多。< / p>

答案 2 :(得分:2)

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

在上面的例子中,关键区域中的代码将一个对象替换为另一个对象,这是一个问题。锁定在对象上,而不是引用,因此更改对象是禁止的。

答案 3 :(得分:0)

实际上,您的第二个代码也不是线程安全的。请使用下面的代码自行检查(你会发现第一个print语句有时会是2,这意味着synchronized块中有两个线程!)。 底线:代码段 - 1&amp;代码片段 - 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();
    }
}