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

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;
        }
    }        
}

我在第一个代码片段中遇到了竞态条件。我明白这是因为我在一个不可变对象(类型为Integer)上获得了锁。
我写了第二个代码片段,它再次不受“Boolean”不可变的影响。但这次可以正常工作(没有输出运行时的竞态条件)。如果我正确理解了先前的问题的解决方案,下面是可能出错的一种方式。
  1. 线程1获取了一个对象的锁(假设是A)并指向了won
  2. 线程2试图获取won所指向的对象的锁,然后进入A的等待队列
  3. 线程1进入同步块,验证A为false,并通过won=true创建一个新对象(假设为B),A认为它赢得了比赛。
  4. 'won'现在指向B。线程1释放了对象A上的锁(不再指向won
  5. 现在,处于对象A等待队列中的线程2被唤醒并获取了对象A的锁,该锁仍然是false(不可变的)。它现在进入同步块并假设自己也赢了,这是不正确的。

为什么第二个代码片段总是正常工作??


这与你之前的问题有何不同? - Mitch Wheat
3
这个代码示例看起来相似但似乎表现不同。在我的书中,这是一个合理的后续问题。 - Jens Schauder
4
@luketorjussen 这不是真的。Immutable 意味着一旦创建对象就不能更改其状态。例如,在 String、Integer、Boolean 等中不存在可以更改对象的 set 方法。Immutable 与 final 无关。 - Multithreader
2
一个令人困惑的点是,这两个示例中都进行了隐式装箱。这会导致对变量的任何“修改”都将用新对象替换原对象。上述两个示例都存在这个问题(用新对象替换锁对象),只是第二个示例没有需要同步的内容(可以完全删除synchronized语句而不会产生外部影响),因此该问题永远不会产生“意外”的结果。 - Hot Licks
@luketorjussen 以这种方式考虑一下。假设你有一辆红色的汽车。现在你不喜欢这个颜色,想把它改成黄色。你有哪些选择?1. 你可以把它喷成黄色。或者2. 你可以买一辆新的黄色汽车。第一种选择代表“可变”,第二种选择代表“不可变”。换句话说,如果第一种选择对你可用,那么你可以说汽车“对象”是可变的。然而,如果第一种选择对你不可用,那么你可以说汽车“对象”是不可变的。 - Multithreader
显示剩余7条评论
4个回答

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

这里存在一个短暂的竞态条件,因为在第一次执行“run”方法后它就消失了,所以你可能没有注意到。此后,“won”变量不断指向代表“true”的Boolean实例,从而适当地充当互斥锁。
这并不意味着你应该在真实项目中编写这样的代码。所有锁对象都应分配给final变量,以确保它们永远不会更改。

1
确认一下,您的意思是说,在第一个线程(进入同步块)测试指向won的对象并将其设置为另一个对象之间有一个非常小的窗口,可能会出现问题。对吧?之后,won始终指向一个真实的对象,因此不会出现竞争条件。 - Abhijith Madhav
1
即使发生这种情况,两个线程都会将完全相同的对象分配给won变量,因此即使考虑到这一点,也可以认为这是线程安全的——但仅限于您提供的文字形式,没有其他代码在synchronized块内。 - Marko Topolnik
嗯...我无法理解您的澄清。您能否告诉我在我的问题中提供的插图中,我分析错误的确切位置是哪里? - Abhijith Madhav
1
在你的分析中,你首先将A赋值为初始的“false”对象,然后说“它仍然是‘true’”,但那只是一个次要的观点。除此之外,你的分析都是正确的,除了你期望第二个线程在任何情况下都应该读取“false”。这只有在你使用ThreadLocal的情况下才可能实现。 - Marko Topolnik
1
也许你也忽略了这一点:当第二个线程进入synchronized块时,它会重新读取won的当前值并得到true。它持有早期won值的锁并不重要。 - Marko Topolnik
没错,那正是我所缺少的。谢谢。我现在明白了。顺便说一下,我已经把“仍然为true”改正为“仍然为false”。 - Abhijith Madhav

2
我遇到了第一个代码片段的竞争条件。我明白这是因为我在一个不可变对象(类型为Integer)上获取了锁。
实际上,这根本不是原因。在不可变对象上获取锁会“正常”工作。问题在于它可能不会做任何有用的事情...
第一个示例失败的真正原因是你锁定了错误的东西。当你执行这个命令 - nRequests++ - 你实际上正在执行等效于这个非原子序列:
    int temp = nRequests.integerValue();
    temp = temp + 1;
    nRequests = Integer.valueOf(temp);

换句话说,您正在为静态变量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 == falsewon == true的转换只发生一次...所以潜在的竞态条件发生的可能性非常小。

2
无论一个对象是否为不可变对象,都与它是否适合作为同步代码块中的锁对象无关。然而,重要的是所有进入同一组临界区的线程使用相同的对象(因此最好将对象引用设为 final),但可以修改对象本身而不影响其“锁定性”。另外,两个或多个不同的同步代码块可以使用不同的引用变量并仍然互斥,只要这些不同的引用变量都引用同一个对象即可。
在上述示例中,关键区域中的代码用另一个对象替换了一个对象,这是有问题的。锁定的是对象而不是引用,因此更改对象是不允许的。

0
实际上,你的第二段代码也不是线程安全的。请使用下面的代码自行检查(你会发现第一个打印语句有时会是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();
    }
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接