【问题标题】:In Java critical sections, what should I synchronize on?在 Java 关键部分中,我应该同步什么?
【发布时间】:2010-09-29 19:02:21
【问题描述】:

在 Java 中,在代码中声明临界区的惯用方式如下:

private void doSomething() {
  // thread-safe code
  synchronized(this) {
    // thread-unsafe code
  }
  // thread-safe code
}

几乎所有块都在this 上同步,但这有什么特别的原因吗?还有其他可能性吗?关于要同步的对象是否有任何最佳实践? (比如Object的私有实例?)

【问题讨论】:

    标签: java multithreading synchronized


    【解决方案1】:

    我尽量避免在this 上进行同步,因为这将允许外部引用该对象的每个人阻止我的同步。相反,我创建了一个本地同步对象:

    public class Foo {
        private final Object syncObject = new Object();
        …
    }
    

    现在我可以使用该对象进行同步,而不必担心任何人“窃取”锁。

    【讨论】:

    • 谁会试图在您的对象上“阻止您的同步”,以致在此同步会变得危险、不切实际或无法使用?
    • 一定要使堆栈跟踪更具可读性,您可能希望为锁定对象的类指定一个名称,以显示它是哪个锁:private static class Lock { }; private final Object lock = new Lock();
    • 如果您的程序有错误,请调试。如果您对多线程程序中正在发生的事情没有真正的线索,那么我非常怀疑避免 synchronized(this) 是否会对您有所帮助。
    • @eljenso - 我同意。锁定“this”可以让外部调用者“原子化”多个方法调用,这是非常宝贵的;考虑迭代一个同步的集合;或者是 PrintWriters 使用私有锁的痛苦 - 曾经尝试过不间断地编写堆栈跟踪?
    • 盲目锁定“this”的另一个问题是,您可能有多个方法在同一个锁上竞争,这些方法在逻辑上不需要互斥。
    【解决方案2】:

    您需要在可用作互斥体的对象上进行同步。如果当前实例(this 引用)合适(例如,不是单例),则可以使用它,就像在 Java 中任何对象都可以用作互斥锁一样。

    在其他情况下,您可能希望在多个类之间共享一个 Mutex,如果这些类的实例可能都需要访问相同的资源。

    这在很大程度上取决于您工作的环境和您正在构建的系统类型。在我见过的大多数 Java EE 应用程序中,实际上并没有真正需要同步...

    【讨论】:

      【解决方案3】:

      首先,注意下面的代码sn-ps是一样的。

      public void foo() {
          synchronized (this) {
              // do something thread-safe
          }
      }
      

      和:

      public synchronized void foo() {
          // do something thread-safe
      }
      

      完全相同的事情。除了代码可读性和样式外,对其中任何一个都没有偏好。

      当您同步方法或代码块时,重要的是要知道为什么您要这样做,以及您要锁定的什么对象,以及对于什么目的。

      另请注意,在某些情况下,您希望客户端同步代码块,其中您要求的监视器(即同步对象)不一定是this,就像在这个例子中:

      Vector v = getSomeGlobalVector();
      synchronized (v) {
          // some thread-safe operation on the vector
      }
      

      我建议您了解更多有关并发编程的知识,一旦您确切了解幕后发生的事情,它将对您有很大帮助。你应该看看Concurrent Programming in Java,一本关于这个主题的好书。如果您想快速深入了解该主题,请查看Java Concurrency @ Sun

      【讨论】:

      • 代码 sn-ps 逻辑上做同样的事情,但它们编译成不同的字节码!!
      • @jason 指向有关 DCL 的文档,因为“它指出编译器或 JMM 可能会做意想不到的事情”是矫枉过正的,并不是真正的重点。您必须指定为什么它们不等效。大多数人都同意他们是等价的:stackoverflow.com/questions/417285
      • 请注意,同步方法通常不是最佳选择,因为我们在方法运行的整个过程中都持有锁。如果它包含耗时但线程安全的部分,以及不那么耗时的线程不安全部分,则同步方法是非常错误的。
      • 最明显的区别是 synchronized(this) 块编译为比同步方法更长的字节码。当您编写同步方法时,编译器只是在方法上放置一个标志,JVM 在看到该标志时获取锁。当您使用 synchronized(this) 块时,编译器会生成类似于 try-finally 块的字节码,该块获取和释放锁并将其内联到您的方法中。
      【解决方案4】:

      Java 中的同步通常涉及在同一实例上同步操作。然后在this 上同步是非常惯用的,因为this 是一个共享引用,可在类中的不同实例方法(或部分)之间自动使用。

      例如,通过声明和初始化私有字段Object lock = new Object() 来使用另一个专门用于锁定的引用,这是我从未需要或使用过的东西。我认为它仅在您需要对对象内的两个或多个未同步资源进行外部同步时才有用,尽管我总是会尝试将这种情况重构为更简单的形式。

      无论如何,隐式(同步方法)或显式synchronized(this) 被大量使用,在Java 库中也是如此。这是一个很好的习惯用法,如果适用,应该始终是您的首选。

      【讨论】:

        【解决方案5】:

        只是为了强调 Java 中也有 ReadWriteLocks 可用,发现为 java.util.concurrent.locks.ReadWriteLock。

        在我的大部分使用中,我将锁定分为“用于阅读”和“用于更新”。如果您仅使用同步关键字,则对同一方法/代码块的所有读取都将被“排队”。一次只有一个线程可以访问该块。

        在大多数情况下,如果您只是在阅读,您永远不必担心并发问题。正是在您进行写入时,您才需要担心并发更新(导致数据丢失)或在写入期间读取(部分更新)。

        因此,在多线程编程期间,读/写锁对我来说更有意义。

        【讨论】:

        【解决方案6】:

        正如之前的回答者所指出的,最佳做法是在有限范围的对象上进行同步(换句话说,选择您可以摆脱的最严格的范围并使用它。)特别是在 this 上进行同步是个坏主意,除非您打算让您的班级的用户获得锁定。

        但是,如果您选择在java.lang.String 上进行同步,则会出现一个特别难看的情况。字符串可以(并且在实践中几乎总是)被实习。这意味着在 ENTIRE JVM 中每个内容相同的字符串在幕后都是相同的字符串。这意味着如果您在任何字符串上进行同步,另一个(完全不同的)代码部分也锁定了具有相同内容的字符串,实际上也会锁定您的代码。

        我曾经对生产系统中的死锁进行故障排除,并且(非常痛苦地)将死锁跟踪到两个完全不同的开源包,每个包都在一个 String 实例上同步,其内容都是 "LOCK"

        【讨论】:

        • +1 了解有关 String 实例锁的真实轶事。
        • 字符串只有在被 intern() (特殊情况)故意实习或用代码编写时才会被实习
        【解决方案7】:

        就个人而言,我认为坚持在this 上同步永远不会或很少正确的答案是错误的。我认为这取决于您的 API。如果你的类是一个线程安全的实现并且你记录了它,那么你应该使用this。如果同步不是为了使类的每个实例在调用它的公共方法时作为一个整体线程安全,那么您应该使用私有内部对象。可重用的库组件通常属于前一类 - 在不允许用户将您的 API 包装在外部同步中之前,您必须仔细考虑。

        在前一种情况下,使用this 允许以原子方式调用多个方法。一个例子是 PrintWriter,您可能想要输出多行(例如到控制台/记录器的堆栈跟踪)并保证它们一起出现 - 在这种情况下,它在内部隐藏同步对象的事实是一个真正的痛苦。另一个这样的例子是同步的集合包装器——你必须在集合对象本身上同步才能进行迭代;因为迭代由多个方法调用组成,您不能完全在内部保护它。

        在后一种情况下,我使用普通对象:

        private Object mutex=new Object();
        

        但是,在看到许多 JVM 转储和堆栈跟踪表明锁是“java.lang.Object() 的实例”之后,我不得不说,正如其他人所建议的那样,使用内部类通常可能更有帮助。

        不管怎样,这就是我的两分钱。

        编辑:另一件事,在this 上同步时,我更喜欢同步方法,并保持方法非常精细。我认为它更清晰,更简洁。

        【讨论】:

        • 锁定 java.lang.String 是个坏主意——因为字符串会被拘留。结果是其他完全不同的代码可能最终(无意地)锁定在您的锁上。锁定名称具有描述性的内部类是完全合理的。 +1 如果该建议被删除。
        • @Jared:请注意,我指定了 new String("xxx"),而不仅仅是 "xxx"。我很清楚字符串的危险性。
        • “在对象本身上同步”和“在为同步目的而显式公开的引用上同步”之间存在差异。如果您需要多个客户端进行同步,则公开一个引用供他们使用。这使得发生的事情更加明显。仍然不需要锁定“this”。
        【解决方案8】:

        您同步的内容取决于可能与此方法调用发生冲突的其他线程可以同步哪些内容。

        如果this 是一个仅由一个线程使用的对象,并且我们正在访问一个在线程之间共享的可变对象,则一个很好的候选者是在该对象上同步 - 在this 上同步没有任何意义,因为另一个修改该共享对象的线程可能甚至不知道this,但确实知道该对象。

        另一方面,如果许多线程同时调用此对象的方法,例如,如果我们处于单例中,则通过 this 进行同步是有意义的。

        请注意,同步方法通常不是最佳选择,因为我们在方法运行的整个过程中都持有锁。如果它包含耗时但线程安全的部分,以及不那么耗时的线程不安全部分,则通过该方法进行同步是非常错误的。

        【讨论】:

          【解决方案9】:

          几乎所有块都在此同步,但这有什么特别的原因吗?还有其他可能吗?

          此声明同步整个方法。

          private synchronized void doSomething() {
          

          这个声明同步了部分代码块而不是整个方法。

          private void doSomething() {
            // thread-safe code
            synchronized(this) {
              // thread-unsafe code
            }
            // thread-safe code
          }
          

          来自 oracle 文档page

          使这些方法同步有两个效果:

          首先,同一对象上的同步方法的两次调用不可能交错。当一个线程正在为一个对象执行同步方法时,所有其他为同一对象调用同步方法的线程都会阻塞(暂停执行),直到第一个线程处理完该对象。

          还有其他可能性吗?关于要同步的对象是否有任何最佳实践? (比如 Object 的私有实例?)

          同步有许多可能性和替代方法。您可以通过使用高级并发 APIs(自 JDK 1.5 版本开始提供)使您的代码线程安全

          Lock objects
          Executors
          Concurrent collections
          Atomic variables
          ThreadLocalRandom
          

          有关详细信息,请参阅以下 SE 问题:

          Synchronization vs Lock

          Avoid synchronized(this) in Java?

          【讨论】:

            【解决方案10】:

            最佳实践是只创建一个对象来提供锁:

            private final Object lock = new Object();
            
            private void doSomething() {
              // thread-safe code
              synchronized(lock) {
                // thread-unsafe code
              }
              // thread-safe code
            }
            

            这样做是安全的,没有调用代码可以通过无意的synchronized(yourObject) 行使您的方法死锁。

            感谢@jared 和@yuval-adam,他们在上面更详细地解释了这一点。

            我的猜测是,在教程中使用 this 的流行来自早期的 Sun javadoc:https://docs.oracle.com/javase/tutorial/essential/concurrency/locksync.html

            【讨论】:

              【解决方案11】:

              同步包括 3 个部分:原子性、可见性和有序性

              同步块是非常粗略的同步级别。它按照您的预期强制执行可见性和排序。但是对于原子性,它并没有提供太多的保护。原子性需要程序的全球知识而不是本地知识。 (这使得多线程编程非常困难)

              假设我们有一个类Account 具有方法depositwithdraw。它们都是基于这样的私有锁同步的:

              class Account {
                  private Object lock = new Object();
              
                  void withdraw(int amount) {
                      synchronized(lock) {
                          // ...
                      }
                  }
              
                  void deposit(int amount) {
                      synchronized(lock) {
                          // ...
                      }
                  }
              }
              

              考虑到我们需要实现一个更高级别的类来处理传输,如下所示:

              class AccountManager {
                  void transfer(Account fromAcc, Account toAcc, int amount) {
                      if (fromAcc.getBalance() > amount) {
                          fromAcc.setBalance(fromAcc.getBalance() - amount);
                          toAcc.setBalance(toAcc.getBalance + amount);
                      }
                  }
              }
              

              假设我们现在有 2 个帐户,

              Account john;
              Account marry;
              

              如果Account.deposit()Account.withdraw() 仅使用内部锁锁定。当我们有 2 个线程工作时,这将导致问题:

              // Some thread
              void threadA() {
                  john.withdraw(500);
              }
              
              // Another thread
              void threadB() {
                  accountManager.transfer(john, marry, 100);
              }
              

              因为threadAthreadB 可以同时运行。并且线程B完成条件检查,线程A退出,线程B再次退出。这意味着即使他的账户没有足够的钱,我们也可以从约翰那里提取 100 美元。这会破坏原子性。

              您可能会建议:那为什么不将withdraw()deposit() 添加到AccountManager 中呢?但是在这个提议下,我们需要创建一个多线程安全的Map,它从不同的账户映射到他们的锁。我们需要在执行后删除锁(否则会泄漏内存)。我们还需要确保没有其他人直接访问Account.withdraw()。这会引入很多细微的错误。

              正确且最惯用的方法是在Account 中公开锁。并让AccountManager 使用锁。但是在这种情况下,为什么不直接使用对象本身呢?

              class Account {
                  synchronized void withdraw(int amount) {
                      // ...
                  }
              
                  synchronized void deposit(int amount) {
                      // ...
                  }
              }
              
              class AccountManager {
                  void transfer(Account fromAcc, Account toAcc, int amount) {
                      // Ensure locking order to prevent deadlock
                      Account firstLock = fromAcc.hashCode() < toAcc.hashCode() ? fromAcc : toAcc;
                      Account secondLock = fromAcc.hashCode() < toAcc.hashCode() ? toAcc : fromAcc;
              
                      synchronized(firstLock) {
                          synchronized(secondLock) {
                              if (fromAcc.getBalance() > amount) {
                                  fromAcc.setBalance(fromAcc.getBalance() - amount);
                                  toAcc.setBalance(toAcc.getBalance + amount);
                              }
                          }
                      }
                  }
              }
              

              简单地说,私有锁不适用于稍微复杂的多线程程序。

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 2021-07-25
                • 2016-01-23
                • 1970-01-01
                • 2019-10-11
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多