
本文深入探讨了java中嵌套`synchronized`块的冗余与必要性问题,特别是在一个方法已通过`synchronized`关键字同步,而其内部又对私有字段进行`synchronized`锁定的场景。我们将分析在何种条件下内部同步块是多余的,以及在何种条件下它对于确保跨不同同步上下文的线程安全至关重要,并提供相应的代码示例和最佳实践建议。
在Java并发编程中,synchronized关键字是实现线程安全的重要工具。它能够确保在同一时间只有一个线程可以执行特定的代码块或方法。然而,不恰当或冗余的同步可能会引入不必要的性能开销,甚至导致死锁。本文将以一个具体的代码示例出发,探讨在何种情况下,对私有字段的嵌套同步是多余的,又在何种情况下它是必不可少的。
考虑以下Java代码片段,其中flushSink()方法被声明为synchronized,这意味着它在Wrapper实例上获取锁。在该方法内部,如果sink字段不为空,又对sink对象本身进行了synchronized锁定。
public class Wrapper {
  private volatile Sink sink;
  public synchronized void flushSink() { // 外部同步:锁定 Wrapper 实例
    if (sink != null) {
      synchronized (sink) { // 内部同步:锁定 sink 实例
        sink.flush();
      }
    }
  }
  public void close()  throws IOException {
    var sink = this.sink;
    if (sink != null) {
      sink.receivedLast();
    }
  }
}初看之下,这种嵌套的synchronized (sink)块可能显得多余。如果两个线程同时调用flushSink(),由于flushSink()方法本身是synchronized的,它们将竞争Wrapper实例的锁。只有一个线程能成功进入flushSink()方法体,从而独占对sink字段的访问。在这种情况下,似乎没有其他线程能够同时访问并修改sink对象,那么对sink的内部同步似乎就没有必要了。
基于这种理解,代码可以被简化为:
立即学习“Java免费学习笔记(深入)”;
public class Wrapper {
  private volatile Sink sink;
  public synchronized void flushSink() {
    // 使用局部变量避免竞态条件,确保在检查非空后操作的是同一个 sink 实例
    var currentSink = this.sink; 
    if (currentSink != null) {
      currentSink.flush();
    }
  }
}在这里,volatile关键字确保了sink引用的可见性,即一个线程对sink的写入(例如,将其设置为新实例或null)对其他线程是立即可见的。将this.sink读取到局部变量currentSink可以避免在if (currentSink != null)检查之后,currentSink在调用flush()之前被另一个线程设置为null的竞态条件。在这种特定情境下,如果Wrapper类中所有对sink字段的操作都通过synchronized方法(即锁定Wrapper实例)来保护,那么内部的synchronized (sink)确实是冗余的。
然而,上述简化并非总是正确的。内部的synchronized (sink)块的必要性,取决于sink对象是否在其他不与Wrapper实例同步的代码路径中被用作锁。
考虑以下修改后的close()方法:
public class Wrapper {
  private volatile Sink sink;
  public synchronized void flushSink() {
    if (sink != null) {
      synchronized (sink) { // 内部同步:锁定 sink 实例
        sink.flush();
      }
    }
  }
  public void close()  throws IOException {
    var currentSink = this.sink;
    if (currentSink != null) {
      synchronized(currentSink) { // 对 sink 实例进行同步
        currentSink.receivedLast();
      }
    }
  }
}在这个修改后的close()方法中,我们引入了synchronized(currentSink)块来保护currentSink.receivedLast()的调用。现在,情况发生了变化:
在这种情况下,flushSink()方法中的synchronized (sink)块就变得至关重要了。如果没有它,一个线程可能正在执行flushSink()(持有Wrapper实例的锁,但没有sink实例的锁),而另一个线程可能正在执行close()(持有sink实例的锁)。这将导致sink.flush()和sink.receivedLast()方法在sink实例上发生并发调用,从而可能破坏Sink对象的内部状态,造成不可预测的行为或数据损坏。
通过在flushSink()中也对sink实例进行同步,我们确保了对sink对象的任何操作(无论是flush()还是receivedLast())都必须先获取sink对象的锁。这样,flush()和receivedLast()的调用就能够实现互斥,即使它们是从不同的外部同步上下文(一个在Wrapper实例上同步,另一个直接在sink实例上同步)发起的。
最佳实践建议:
理解synchronized锁的作用范围和对象生命周期对于编写正确且高效的并发代码至关重要。在决定是否使用嵌套同步时,关键在于分析所有可能访问共享资源的线程路径,并确保所有相关操作都在一个一致的同步机制下进行。
以上就是Java中嵌套同步块的冗余与必要性分析的详细内容,更多请关注php中文网其它相关文章!
 
                        
                        每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
 
                Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号