在Java关键部分中,应该同步哪些内容?


73

在Java中,在代码中声明关键部分的惯用方式如下:

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

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

Answers:


50

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

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
}

我建议您了解有关并发编程的更多知识,一旦您确切了解幕后发生的事情,它将为您提供大量帮助。您应该阅读《Java并行编程》,这是一本关于该主题的好书。如果您想快速了解该主题,请查看Java Concurrency @Sun。


1
代码段在逻辑上做同样的事情,但是它们编译为不同的字节码!
尼尔·科菲


2
@jason指向有关DCL的文档,因为“它指出编译器或JMM可能会执行意外的事情”,这是过大的,并不是说到重点。您必须指定为什么它们不相等。大多数人都认为它们是等效的:stackoverflow.com/questions/417285
eljenso,2009年

2
请注意,同步方法通常不是最佳选择,因为在整个方法运行期间我们都会持有一个锁。如果它包含耗时但线程安全的部分,而不是那么耗时的线程不安全部分,则同步方法是非常错误的。
汉斯·彼得·斯特尔

4
最明显的区别是,已同步(this)块编译为比已同步方法更长的字节码。当您编写同步方法时,编译器只是在该方法上放置一个标志,而JVM在看到该标志时会获取该锁。当您使用synced(this)块时,编译器将生成类似于try-finally块的字节码,该块将获取并释放该锁并将其内联到您的方法中。
Andrew

70

正如早期的答复者所指出的,最佳实践是在有限范围的对象上进行同步(换句话说,选择可以使用的限制性最强的范围,并使用它。)特别是,进行同步this不是一个好主意,除非您打算允许班级的用户获得锁。

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

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


17
+1是有关String实例锁的实用实用轶事。
Ogre Psalm33,2011年

1
仅当字符串被intern()故意插入(特殊情况)或使用代码编写时,
才对

43

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

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

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


9
谁会试图以某种方式“阻止您的同步”,从而使同步变得危险,不切实际或无法使用?
eljenso

8
要使堆栈跟踪更具可读性,您可能想给锁定对象的类一个名称,以显示它是哪个锁:private static class Lock { }; private final Object lock = new Lock();
Tom Hawtin-定位线

6
如果程序中有错误,请进行调试。我非常怀疑这样一个事实:如果您尚未对多线程程序中正在发生的事情有真正的了解,那么避免使用sync(this)将对您有所帮助。
eljenso

2
@eljenso-我帮忙 锁定“ this”可以允许外部调用者“原子化”多个方法调用,这是非常宝贵的;考虑迭代一个同步的集合;还是使用专用锁的PrintWriters的痛苦-曾经尝试编写堆栈跟踪而不中断吗?
劳伦斯·多尔

5
盲目地锁定“ this”的另一个问题是,您可能有几种方法争用同一个锁,这些方法在逻辑上不需要互斥。
sk。

6

只是为了强调,还有Java中可用的ReadWriteLocks,可以找到java.util.concurrent.locks.ReadWriteLock。

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

在大多数情况下,只要阅读即可,您不必担心并发问题。当您在进行写操作时,您会担心并发更新(导致数据丢失)或在写操作期间进行读取(部分更新)。

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



4

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

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

这在很大程度上取决于您所使用的环境以及所构建的系统类型。在我见过的大多数Java EE应用程序中,实际上并不需要同步...


4

就我个人而言,我认为坚持永远不正确或很少正确同步的答案this是错误的。我认为这取决于您的API。如果您的类是线程安全的实现,因此您要对其进行记录,则应使用this。如果同步不是要在调用其公共方法时使该类的每个实例成为整个线程安全的对象,则应使用私有内部对象。可重用的库组件通常属于前一类-在禁止用户将API包装在外部同步中之前,必须仔细考虑。

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

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

private Object mutex=new Object();

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

无论如何,这就是我的两点价值。

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


1
锁定java.lang.String是一个糟糕的主意-字符串会被嵌入。结果是其他完全不同的代码可能最终(无意间)锁定您的锁。锁定名称为描述性的内部类是完全合理的。如果该建议已被删除,则+1。
杰瑞德(Jared)2009年

@Jared:请注意,我指定了新的String(“ xxx”),而不仅仅是“ xxx”。我很清楚实习生弦的危险。
劳伦斯·多尔

1
“在对象本身上同步”和“在为同步目的显式公开的引用上同步”之间有区别。如果您需要多个客户端进行同步,请公开参考供他们使用。这使得发生的事情更加明显。仍然无需锁定“此”。
乔恩·斯基特

2

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

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

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


1

同步的方式取决于与此方法调用可能潜在冲突的其他线程可以同步。

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

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

请注意,同步方法通常不是最佳选择,因为在整个方法运行期间我们都会持有一个锁。如果它包含耗时但线程安全的部分,而不是耗时的线程不安全部分,则在方法上进行同步是非常错误的。


1

几乎所有块都与此同步,但是是否有特定原因呢?还有其他可能性吗?

该声明使整个方法同步。

private synchronized void doSomething() {

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

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

从oracle文档页面

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

首先,不可能在同一对象上对同步方法的两次调用进行交错。当一个线程正在执行对象的同步方法时,所有其他调用同一对象块的同步方法的线程(挂起执行),直到第一个线程用该对象完成。

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

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

Lock objects
Executors
Concurrent collections
Atomic variables
ThreadLocalRandom

有关更多详细信息,请参阅下面的SE问题:

同步与锁定

避免在Java中同步(this)?


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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.