75

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

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

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

4

11 回答 11

73

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

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

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

于 2009-01-13T16:12:21.980 回答
51

首先,请注意以下代码片段是相同的。

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

于 2009-01-06T11:47:43.880 回答
45

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

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

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

于 2009-01-06T11:35:59.130 回答
6

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

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

在大多数情况下,如果您只是在阅读,您永远不必担心并发问题。当你在写的时候你担心并发更新(导致数据丢失),或者在写期间读(部分更新),你必须担心。

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

于 2009-01-06T14:41:41.487 回答
4

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

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

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

于 2009-01-06T11:37:20.923 回答
4

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

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

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

private Object mutex=new Object();

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

无论如何,这是我的两分钱。

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

于 2009-01-15T07:35:12.037 回答
2

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

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

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

于 2009-01-06T12:26:53.520 回答
1

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

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

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

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

于 2010-06-23T13:43:55.783 回答
1

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

此声明同步整个方法。

private synchronized void doSomething() {

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

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

从 oracle 文档页面

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

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

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

同步有许多可能性和替代方案。您可以使用高级并发API(自 JDK 1.5 版本起可用)使您的代码线程安全

Lock objects
Executors
Concurrent collections
Atomic variables
ThreadLocalRandom

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

同步与锁定

在 Java 中避免同步(this)?

于 2016-04-19T16:21:43.500 回答
1

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

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

于 2019-05-10T01:01:47.063 回答
0

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

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

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

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

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

您可能会建议:为什么不添加withdraw()anddeposit()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);
                }
            }
        }
    }
}

简而言之,私有锁不适用于稍微复杂的多线程程序。

于 2021-06-07T19:29:27.743 回答