【问题标题】:Synchronization on immutable objects (in java)不可变对象上的同步(在 java 中)
【发布时间】:2026-02-19 12:25:03
【问题描述】:

代码 sn-p - 1

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

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

代码 sn-p - 2

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

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

我遇到了第一个代码 sn-p 的竞争条件。我understood 这是因为我正在获得一个不可变对象(整数类型)的锁。

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

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

为什么第二个代码 sn-p 一直运行良好??

【问题讨论】:

  • 这与您之前的问题有何不同?
  • 此代码示例看起来相似,但(似乎)表现不同。我书中有效的后续问题。
  • 我想你误解了不可变的含义。要使某些东西不可变,您应该将其标记为 final
  • @luketorjussen 这不是真的。不可变意味着一旦创建,您就无法更改对象的状态。例如,没有 set 方法可以像 String、Integer、Boolean 那样更改对象... 不可变与 final 无关
  • 一个令人困惑的地方是隐式装箱在两个示例中都在进行。这会导致变量的任何“修改”以用新对象替换对象。上面两个例子都有这个问题(用一个新的替换锁对象),只是第二个例子没有需要同步的东西(可以完全删除synchronized语句而没有外部影响),所以这个问题永远不会产生“意外”的结果。

标签: java multithreading synchronization


【解决方案1】:
    synchronized (won)
    {
        if (!won)
        won = true;
    }

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

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

【讨论】:

  • 确认一下,您的意思是说有一个非常小的窗口可能会出错。在第一个线程(进入同步块)之间测试won 指向的对象,然后将其设置为另一个对象。对?之后won 总是指向一个真实的对象,因此没有竞争条件。
  • 即使发生这种情况,两个线程也会将完全相同的对象分配给won 变量,因此即使考虑到这一点,这也可以被视为线程安全的---但仅 以您呈现的文字形式,synchronized 块中没有其他代码。
  • 嗯...我无法理解您的说明。你能告诉我在插图中的确切位置(我在我的问题中提供的)我分析错了吗?
  • 在您的分析中,您首先将 A 指定为最初的 false 对象,然后说“仍然是 true”,但这是一个侧面。除此之外,您的分析都是正确的,除了您期望第二个线程在任何情况下都应该读取false。仅当您使用 ThreadLocal 时才有可能。
  • 也许你也错过了这一点:当第二个线程进入synchronized 块时,它重新读取won 的当前值并获得true。它持有won的早期值的锁并不重要。
【解决方案2】:

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

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

【讨论】:

    【解决方案3】:

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

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

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

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

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

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

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

    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 的转换只发生了一次......所以潜在的竞争条件实际上最终发生的可能性要小得多。

    【讨论】:

      【解决方案4】:

      事实上,您的第二个代码也不是线程安全的。请使用下面的代码自行检查(您会发现第一个 print 语句有时会是 2,这意味着同步块内有两个线程!)。 底线:代码 sn-p - 1 和代码 sn-p - 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();
          }
      }
      

      【讨论】: