每当有关Java同步的问题出现时,有些人非常渴望指出synchronized(this)
应该避免的.相反,他们声称,首选锁定私人参考.
一些给出的原因是:
一些邪恶的代码可能会偷你的锁(这个非常受欢迎,也有一个"意外"变种)
同一类中的所有同步方法使用完全相同的锁,这会降低吞吐量
你(不必要地)暴露了太多的信息
其他人,包括我在内,认为这synchronized(this)
是一个被大量使用的习惯用法(也在Java库中),是安全且易于理解的.它不应该被避免,因为你有一个错误,你不知道多线程程序中发生了什么.换句话说:如果适用,则使用它.
我有兴趣看到一些现实世界的例子(没有foobar的东西)避免锁定this
是最好的,当synchronized(this)
也做的工作.
因此:您是否应始终避免synchronized(this)
并使用私有引用上的锁来替换它?
一些进一步的信息(更新为答案):
我们正在谈论实例同步
考虑隐式(synchronized
方法)和显式形式synchronized(this)
如果您引用Bloch或其他有关该主题的权限,请不要遗漏您不喜欢的部分(例如,有效Java,线程安全项目:通常它是实例本身的锁,但也有例外.)
如果你需要锁定的粒度而不是synchronized(this)
提供,那么synchronized(this)
不适用,所以这不是问题
Darron.. 125
我将分别介绍每一点.
一些邪恶的代码可能会偷你的锁(这个非常受欢迎,也有一个"意外"变种)
我不小心担心.它相当于这种使用this
是你的类暴露界面的一部分,应该记录在案.有时需要其他代码使用锁的能力.对于类似的事情Collections.synchronizedMap
(见javadoc)也是如此.
同一类中的所有同步方法使用完全相同的锁,这会降低吞吐量
这是过于简单化的思维; 只是摆脱synchronized(this)
不会解决问题.适当的吞吐量同步需要更多考虑.
您(不必要地)暴露了太多信息
这是#1的变体.使用synchronized(this)
是您界面的一部分.如果您不希望/需要暴露,请不要这样做.
基本上同步(this)*被暴露,因为这意味着外部代码会影响您的类的操作.所以我断言你必须将它记录为界面,即使语言没有. (65认同)
类似.请参阅Javadoc for Collections.synchronizedMap() - 返回的对象在内部使用synchronized(this),并且它们希望使用者利用它来对迭代等大规模原子操作使用相同的锁. (15认同)
实际上,Collections.synchronizedMap()在内部未使用synced(this),而是使用了私有的最终锁定对象。 (2认同)
cletus.. 84
那么,首先应该指出:
public void blah() { synchronized (this) { // do stuff } }
在语义上等同于:
public synchronized void blah() { // do stuff }
这是不使用的一个原因synchronized(this)
.你可能会争辩说你可以在synchronized(this)
街区附近做点什么.通常的原因是尝试避免必须进行同步检查,这会导致各种并发问题,特别是双重检查锁定问题,这只是表明进行相对简单的检查是多么困难.线程安全的.
私人锁定是一种防御机制,这绝不是一个坏主意.
此外,正如您所提到的,私有锁可以控制粒度.对象上的一组操作可能与另一组完全无关,但synchronized(this)
会相互排除对所有操作的访问.
synchronized(this)
只是真的不给你任何东西.
我将分别介绍每一点.
一些邪恶的代码可能会偷你的锁(这个非常受欢迎,也有一个"意外"变种)
我不小心担心.它相当于这种使用this
是你的类暴露界面的一部分,应该记录在案.有时需要其他代码使用锁的能力.对于类似的事情Collections.synchronizedMap
(见javadoc)也是如此.
同一类中的所有同步方法使用完全相同的锁,这会降低吞吐量
这是过于简单化的思维; 只是摆脱synchronized(this)
不会解决问题.适当的吞吐量同步需要更多考虑.
您(不必要地)暴露了太多信息
这是#1的变体.使用synchronized(this)
是您界面的一部分.如果您不希望/需要暴露,请不要这样做.
那么,首先应该指出:
public void blah() { synchronized (this) { // do stuff } }
在语义上等同于:
public synchronized void blah() { // do stuff }
这是不使用的一个原因synchronized(this)
.你可能会争辩说你可以在synchronized(this)
街区附近做点什么.通常的原因是尝试避免必须进行同步检查,这会导致各种并发问题,特别是双重检查锁定问题,这只是表明进行相对简单的检查是多么困难.线程安全的.
私人锁定是一种防御机制,这绝不是一个坏主意.
此外,正如您所提到的,私有锁可以控制粒度.对象上的一组操作可能与另一组完全无关,但synchronized(this)
会相互排除对所有操作的访问.
synchronized(this)
只是真的不给你任何东西.
当您使用synchronized(this)时,您正在使用类实例作为锁本身.这意味着当线程1获取锁定时,线程2应该等待
假设以下代码
public void method1() { // do something ... synchronized(this) { a ++; } // ................ } public void method2() { // do something ... synchronized(this) { b ++; } // ................ }
方法1修改变量a和方法2修改变量b,应该避免两个线程同时修改同一个变量.但是当thread1修改a和thread2修改b时,它可以在没有任何竞争条件的情况下执行.
不幸的是,上面的代码不允许这样做,因为我们对锁使用相同的引用; 这意味着线程即使它们不处于竞争状态也应该等待,显然代码牺牲了程序的并发性.
解决方案是为两个不同的变量使用2个不同的锁.
public class Test { private Object lockA = new Object(); private Object lockB = new Object(); public void method1() { // do something ... synchronized(lockA) { a ++; } // ................ } public void method2() { // do something ... synchronized(lockB) { b ++; } // ................ } }
上面的例子使用了更细粒度的锁(2个锁而不是一个(分别是变量a和b的lockA 和lockB),因此结果允许更好的并发性,另一方面它变得比第一个例子更复杂......
虽然我同意不盲目地遵守教条规则,但"锁定窃取"场景对您来说是否如此古怪?一个线程确实可以获取对象"external"(synchronized(theObject) {...}
)的锁定,阻塞等待同步实例方法的其他线程.
如果您不相信恶意代码,请考虑此代码可能来自第三方(例如,如果您开发某种应用程序服务器).
"意外"版本似乎不太可能,但正如他们所说,"做一些傻逼的东西,有人会发明一个更好的白痴".
所以我同意它取决于什么是课堂的思想学派.
编辑以下eljenso的前3条评论:
我从未经历过锁定窃取问题,但这是一个想象的场景:
假设您的系统是一个servlet容器,我们正在考虑的对象就是ServletContext
实现.它的getAttribute
方法必须是线程安全的,因为上下文属性是共享数据; 所以你声明它synchronized
.我们还假设您根据容器实现提供公共托管服务.
我是您的客户,并在您的网站上部署我的"好"servlet.碰巧我的代码包含了一个调用getAttribute
.
黑客伪装成另一个客户,在您的网站上部署他的恶意servlet.它在init
方法中包含以下代码:
synchronized (this.getServletConfig().getServletContext()) { while (true) {} }
假设我们共享相同的servlet上下文(只要这两个servlet位于同一个虚拟主机上,那么规范允许),我的调用getAttribute
将永久锁定.黑客在我的servlet上实现了DoS.
如果getAttribute
在私有锁上同步,则无法进行此攻击,因为第三方代码无法获取此锁.
我承认这个例子是人为的,并且对servlet容器的工作方式过于简单化了,但恕我直言,它证明了这一点.
所以我会根据安全考虑做出我的设计选择:我是否可以完全控制可以访问实例的代码?线程无限期地锁定一个实例的后果是什么?
在C#和Java阵营中似乎有不同的共识. 我见过的大多数Java代码都使用:
// apply mutex to this instance synchronized(this) { // do work here }
而大多数C#代码选择更可靠:
// instance level lock object private readonly object _syncObj = new object(); ... // apply mutex to private instance level field (a System.Object usually) lock(_syncObj) { // do work here }
C#成语当然更安全.如前所述,不能从实例外部对锁进行恶意/意外访问.Java代码也存在这种风险,但似乎Java社区已经越来越倾向于稍微不那么安全但稍微简洁的版本.
这并不意味着对Java的挖掘,只是反映了我在两种语言上工作的经验.
这取决于实际情况.
如果只有一个共享实体或多个共享实体.
请参阅 此处的完整工作示例
一个小介绍.
线程和可共享实体
多个线程可以访问同一个实体,例如,多个connectionThread共享一个messageQueue.由于线程同时运行,可能有可能将一个数据覆盖另一个数据,这可能是混乱的情况.
因此,我们需要某种方法来确保一次只能通过一个线程访问可共享实体.(CONCURRENCY).
同步块
synchronized()块是一种确保可共享实体的并发访问的方法.
首先,一个小类比
假设洗手间内有一个双人P1,P2(螺纹)洗脸盆(可共享的实体),还有一扇门(锁).
现在我们希望一个人一次使用洗脸盆.
一种方法是在门被锁定时通过P1锁定门P2等待直到p1完成他的工作
P1解锁门
然后只有p1可以使用洗脸盆.
句法.
synchronized(this) { SHARED_ENTITY..... }
"this"提供了与类关联的内部锁(Java开发人员设计的Object类,使得每个对象都可以作为监视器工作).当只有一个共享实体和多个线程(1:N)时,上述方法可以正常工作.
N个可共享实体-M个线程
现在想想洗手间内只有一个洗脸盆而且只有一个门的情况.如果我们使用之前的方法,只有p1可以一次使用一个洗脸盆,而p2会在外面等待.由于没有人使用B2(洗脸盆),这是资源的浪费.
一个更明智的方法是在洗手间内创建一个较小的房间,并为每个洗脸盆提供一扇门.这样,P1可以访问B1,P2可以访问B2,反之亦然.
washbasin1; washbasin2; Object lock1=new Object(); Object lock2=new Object(); synchronized(lock1) { washbasin1; } synchronized(lock2) { washbasin2; }
在线程上查看更多信息 ----> 这里
该java.util.concurrent
软件包大大降低了我的线程安全代码的复杂性.我只有轶事证据可以继续,但我见过的大部分工作synchronized(x)
似乎都是重新实现Lock,Semaphore或Latch,而是使用较低级别的监视器.
考虑到这一点,使用任何这些机制进行同步类似于在内部对象上进行同步,而不是泄漏锁.这是有益的,因为您可以绝对确定您通过两个或多个线程控制进入监视器的条目.
如果可能,使数据不可变(final
变量)
如果无法避免跨多个线程共享数据的突变,请使用高级编程构造[例如粒度 Lock
API]
锁提供对共享资源的独占访问:一次只有一个线程可以获取锁,并且对共享资源的所有访问都需要首先获取锁.
ReentrantLock
实现Lock
接口的示例代码
class X { private final ReentrantLock lock = new ReentrantLock(); // ... public void m() { lock.lock(); // block until condition holds try { // ... method body } finally { lock.unlock() } } }
锁定同步的优点(这)
同步方法或语句的使用强制所有锁获取和释放以块结构方式发生.
通过提供,锁实现提供了使用同步方法和语句的附加功能
获取锁定的非阻塞尝试(tryLock()
)
尝试获取可以中断的锁(lockInterruptibly()
)
尝试获取可以超时(tryLock(long, TimeUnit)
)的锁定.
Lock类还可以提供与隐式监视器锁完全不同的行为和语义,例如
保证订购
非重复使用
死锁检测
看看有关各种类型的SE问题Locks
:
同步与锁定
您可以使用高级并发API而不是Synchronied块来实现线程安全.本文档页面提供了良好的编程结构,以实现线程安全.
锁定对象支持锁定习惯用法,简化了许多并发应用程序.
执行程序定义用于启动和管理线程的高级API.java.util.concurrent提供的执行程序实现提供适用于大规模应用程序的线程池管理.
并发集合使管理大量数据更容易,并且可以大大减少同步需求.
Atomic Variables具有最小化同步并有助于避免内存一致性错误的功能.
ThreadLocalRandom(在JDK 7中)提供从多个线程有效生成伪随机数.
对于其他编程结构,也请参阅java.util.concurrent和java.util.concurrent.atomic包.
如果您已经决定:
你需要做的就是锁定当前对象; 和
你想用比整个方法小的粒度锁定它;
然后我没有看到同步的禁忌(这个).
有些人故意在方法的整个内容中使用synchronized(this)(而不是将方法标记为同步),因为他们认为对读者"更清楚"哪个对象实际上是同步的.只要人们做出明智的选择(例如,通过这样做,他们实际上是在方法中插入额外的字节码,这可能会对潜在的优化产生连锁效应),我并不特别看到这个问题. .你应该总是记录你的程序的并发行为,所以我没有看到"'synchronized'发布行为"的论点是如此引人注目.
至于你应该使用哪个对象的锁定的问题,我认为在当前对象上进行同步是没有问题的,如果你正在做的事情的逻辑以及你的类通常如何被使用的话.例如,对于集合,逻辑上期望锁定的对象通常是集合本身.