
本文探讨java中在一个同步方法内部对私有字段进行嵌套同步的必要性。虽然方法级别的同步通常足以保护对字段的访问,但如果该字段本身在其他代码路径中也被作为锁对象进行同步,那么嵌套同步则变得至关重要,以确保对字段所引用的对象的操作原子性和互斥性。
在Java并发编程中,synchronized关键字是实现线程同步和互斥的核心机制。它可以修饰方法或代码块,分别对应对象锁和任意对象锁。当一个方法被synchronized修饰时,它会锁定当前实例(this对象);当一个代码块被synchronized(object)修饰时,它会锁定指定的object对象。理解这些锁的粒度和作用域对于编写正确的并发程序至关重要。
考虑以下一个包含私有字段和同步方法的Wrapper类:
public class Wrapper {
private volatile Sink sink; // sink字段可能被重新赋值,volatile保证可见性
public synchronized void flushSink() {
if (sink != null) {
synchronized (sink) { // 嵌套同步块
sink.flush();
}
}
}
public void close() throws IOException {
var currentSink = this.sink; // 读取到局部变量,避免null检查后sink被设为null
if (currentSink != null) {
currentSink.receivedLast();
}
}
}在上述flushSink()方法中,public synchronized void flushSink()意味着任何线程在执行此方法之前,必须先获取Wrapper实例(即this对象)的锁。这保证了在同一时刻,只有一个线程能够进入flushSink()方法内部。
紧接着,方法内部又有一个synchronized (sink)的嵌套同步块。如果仅从flushSink()方法自身来看,由于方法已经同步,保证了对sink字段的访问是互斥的,那么这个synchronized (sink)似乎是多余的。因为如果sink对象只在flushSink()方法内部被操作,且没有其他地方对sink对象本身进行同步,那么synchronized (sink)看起来是冗余的,可以进行优化。
立即学习“Java免费学习笔记(深入)”;
基于这种理解,一种初步的优化方案是移除嵌套的同步块:
public class Wrapper {
private volatile Sink sink;
public synchronized void flushSink() {
// 移除嵌套同步块,因为方法本身已同步
var currentSink = this.sink; // 良好的实践,确保在后续操作中使用一致的引用
if (currentSink != null) {
currentSink.flush();
}
}
// ... close() 方法保持不变
}这里将sink读取到局部变量currentSink是一个良好的实践。即使sink是volatile的,它只保证了sink引用的可见性,不保证null检查和后续方法调用之间的原子性。虽然在synchronized方法内部,this锁已经提供了强大的保护,但这种局部变量赋值可以增强代码的健壮性,尤其是在更复杂的并发场景或非synchronized方法中。
然而,嵌套同步块的真正必要性,取决于代码库中其他部分如何使用和同步sink字段。如果sink对象在其他方法中也被作为锁对象进行同步,那么嵌套的synchronized (sink)就变得至关重要。
考虑以下close()方法的修改版本:
public class Wrapper {
private volatile Sink sink;
public synchronized void flushSink() {
if (sink != null) {
synchronized (sink) { // 此时嵌套同步块是必要的
sink.flush();
}
}
}
public void close() throws IOException {
var currentSink = this.sink;
if (currentSink != null) {
synchronized(currentSink) { // 对sink对象进行同步
currentSink.receivedLast();
}
}
}
}在这种情况下,flushSink()方法持有Wrapper实例的锁(this),而close()方法则持有Sink实例的锁(currentSink)。synchronized (this)和synchronized (currentSink)是两个不同的锁。
因此,如果需要保证sink.flush()和sink.receivedLast()这两个操作在同一个Sink实例上是互斥的,即它们不能同时发生,那么flushSink()中的嵌套synchronized (sink)就变得不可或缺。它确保了flush()操作在执行时,持有Sink对象的锁,从而阻止其他线程(例如通过close()方法)同时获取Sink对象的锁并执行receivedLast()。
从上述分析可以看出,嵌套同步块并非总是多余的,其必要性取决于共享对象在整个系统中的同步策略和所需保证的原子性级别。
锁的粒度:
全局视角: 在进行同步优化或判断同步块是否冗余时,必须考虑所有可能访问共享资源的路径。仅仅查看一个方法内部是不够的,需要审视整个代码库中该共享对象的所有使用场景。
volatile与synchronized: volatile关键字确保了sink字段引用的可见性,即一个线程对sink引用的修改对其他线程是立即可见的。然而,volatile不能保证对sink对象方法的调用是原子性的或互斥的。synchronized则提供了互斥性(同一时间只有一个线程可以持有锁)和内存可见性(释放锁时会将本地内存刷新到主内存,获取锁时会从主内存读取最新值)。
正确性优先: 在并发编程中,确保并发操作的正确性远比微小的性能优化重要。如果对嵌套同步块的必要性存在疑问,通常更安全的做法是保留它,除非有明确的证据表明其冗余且移除不会引入并发问题。
并发测试: 对于这种复杂的并发行为,Java Concurrency Stress (JCStress) 是一个强大的工具,可以用来编写并发测试以验证同步逻辑的正确性。通过模拟多线程并发访问,JCStress能够发现潜在的竞态条件和内存可见性问题,从而验证重构是否正确。
综上所述,是否消除嵌套的synchronized块,并非简单的“是”或“否”,而是需要根据整个系统的并发设计和对共享资源的操作需求来决定。在进行此类重构时,务必进行全面的分析和严格的测试。
以上就是Java中嵌套同步块的考量:私有字段同步的必要性与优化的详细内容,更多请关注php中文网其它相关文章!
每个人都需要一台速度更快、更稳定的 PC。随着时间的推移,垃圾文件、旧注册表数据和不必要的后台进程会占用资源并降低性能。幸运的是,许多工具可以让 Windows 保持平稳运行。
Copyright 2014-2025 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号