在Java关键部分中,我应该同步什么?
-
03-07-2019 - |
题
在Java中,在代码中声明关键部分的惯用方法如下:
private void doSomething() {
// thread-safe code
synchronized(this) {
// thread-unsafe code
}
// thread-safe code
}
几乎所有的块都在 this
上同步,但这有什么特别的原因吗?还有其他可能吗?是否有关于同步对象的最佳实践? (例如 Object
的私有实例?)
解决方案
首先,请注意以下代码段是相同的。
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
其他提示
正如早期的回答者所指出的,最佳做法是在有限范围的对象上进行同步(换句话说,选择您可以使用的最严格的范围,并使用它。)特别是,在上进行同步这个
是一个坏主意,除非您打算允许类的用户获得锁定。
但是,如果您选择在 java.lang.String
上进行同步,则会出现一个特别难看的情况。字符串可以(实际上几乎总是)实习。这意味着每个相同内容的字符串 - 在整个JVM 中 - 变成幕后的同一个字符串。这意味着如果您在任何String上进行同步,另一个(完全不同的)代码部分也会锁定具有相同内容的String,实际上也会锁定您的代码。
我曾经在生产系统中对死锁进行故障排除,并且(非常痛苦地)跟踪了两个完全不同的开源软件包的死锁,每个软件包都在一个String实例上同步,其内容都是&quot; LOCK&quot;
我尽量避免在 this
上进行同步,因为这样可以让所有来自外部的人都能阻止我的同步。相反,我创建了一个本地同步对象:
public class Foo {
private final Object syncObject = new Object();
…
}
现在我可以使用该对象进行同步而不必担心任何人&#8220;偷窃&#8221;锁。
只是为了强调Java中还有ReadWriteLocks,可以找到java.util.concurrent.locks.ReadWriteLock。
在我的大部分用法中,我将锁定分为“阅读”和“更新”。如果您只使用synchronized关键字,则对同一方法/代码块的所有读取都将“排队”。一次只能有一个线程访问该块。
在大多数情况下,如果您只是在阅读,就不必担心并发问题。当你在写作时,你担心并发更新(导致数据丢失),或者在写入(部分更新)期间阅读,你必须担心。
因此,在多线程编程期间,读/写锁对我来说更有意义。
您需要在可用作互斥锁的对象上进行同步。如果当前实例( this 引用)是合适的(例如,不是Singleton),您可以使用它,就像在Java中一样,任何Object都可以作为Mutex。
在其他情况下,如果这些类的实例都需要访问相同的资源,您可能希望在多个类之间共享互斥锁。
这在很大程度上取决于您所处的环境以及您正在构建的系统类型。在我见过的大多数Java EE应用程序中,实际上并不需要同步...
就我个人而言,我认为在这个
上同步的答案是永远或者很少是正确的,这是错误的。我认为这取决于您的API。如果你的类是一个线程安全的实现,你就这样记录它,那么你应该使用 this
。如果同步不是在调用它的公共方法时使每个类的实例作为一个整体线程安全,那么你应该使用私有内部对象。可重用的库组件通常属于前一类 - 在禁止用户将API包装在外部同步之前,您必须仔细考虑。
在前一种情况下,使用 this
允许以原子方式调用多个方法。一个例子是PrintWriter,您可能希望输出多行(比如堆栈跟踪到控制台/记录器)并保证它们一起出现 - 在这种情况下,它在内部隐藏同步对象这一事实真的很痛苦。另一个这样的例子是同步集合包装器 - 你必须在集合对象本身上进行同步以便迭代;由于迭代包含多个方法调用,因此无法完全在内部保护它。
在后一种情况下,我使用普通对象:
private Object mutex=new Object();
但是,看过许多JVM转储和堆栈跟踪表明锁是“java.lang.Object()”的一个实例。我不得不说,正如其他人所建议的那样,使用内部类可能更有帮助。
无论如何,那是我的两位值。
编辑:另外一件事,当在这个
上进行同步时,我更喜欢同步这些方法,并保持方法非常精细。我认为它更清晰,更简洁。
Java中的同步通常涉及在同一实例上同步操作。然后,在此
上进行同步非常惯用,因为这个
是一个共享引用,可以在类的不同实例方法(或部分)之间自动使用。
使用另一个专门用于锁定的引用,通过声明和初始化私有字段 Object lock = new Object()
,这是我从不需要或使用的东西。我认为只有在对象内部的两个或多个非同步资源上需要外部同步时,它才有用,尽管我总是试图将这种情况重构为更简单的形式。
无论如何,隐式(synchronized方法)或显式 synchronized(this)
在Java库中也被大量使用。这是一个很好的习语,如果适用的话,应该永远是你的第一选择。
关于同步的内容取决于可能与此方法调用冲突的其他线程可以同步。
如果这个
是一个只由一个线程使用的对象,而我们正在访问一个在线程之间共享的可变对象,那么一个好的候选者就是在该对象上进行同步 - 在上进行同步这个
没有意义,因为修改该共享对象的另一个线程可能甚至不知道 this
,但确实知道该对象。
另一方面,通过同步这个
是有意义的,如果许多线程同时调用此对象的方法,例如,如果我们是单个的。
请注意,同步方法通常不是最佳选择,因为我们在方法运行的整个过程中保持锁定。如果它包含耗时但线程安全的部分,并且不是那么耗时的线程不安全的部分,则对该方法进行同步是非常错误的。
几乎所有的块都会在此同步,但这有什么特别的原因吗?还有其他可能性吗?
此声明同步整个方法。
private synchronized void doSomething() {
此声明同步了代码块的一部分而不是整个方法。
private void doSomething() {
// thread-safe code
synchronized(this) {
// thread-unsafe code
}
// thread-safe code
}
来自oracle文档页面
使这些方法同步有两个影响:
首先,对同一对象的两个同步方法的调用不可能交错。当一个线程正在为一个对象执行一个synchronized方法时,所有其他线程都会调用同一个对象的同步方法(暂停执行)直到第一个线程完成该对象。
还有其他可能吗?是否有关于同步对象的最佳实践? (比如Object的私有实例?)
同步有很多种可能性和替代方案。您可以通过使用高级并发性来使代码线程安全 API (自JDK 1.5发布以来可用)
Lock objects
Executors
Concurrent collections
Atomic variables
ThreadLocalRandom
有关详细信息,请参阅以下SE问题:
最佳实践是创建一个仅用于提供锁定的对象:
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