避免在Java中同步(this)?

每当一个关于Java同步的问题popup时,有些人非常希望指出应该避免synchronized(this) 。 相反,他们声称,locking一个私人参考是首选。

一些给定的原因是:

  • 一些邪恶的代码可能会偷你的锁 (非常stream行这一个,也有一个“意外”的变种)
  • 同一类中的所有同步方法使用完全相同的锁,这会降低吞吐量
  • 你(不必要地)暴露太多的信息

包括我在内的其他人认为synchronized(this)是一个习惯用法(在Java库中也是如此),是安全的,并且很好理解。 它不应该被避免,因为你有一个错误,你不知道你的multithreading程序正在发生什么。 换句话说:如果适用,那么就使用它。

我有兴趣看到一些真实世界的例子(没有foobar的东西),避免lockingthis是最好的时候synchronized(this)也可以做这项工作。

因此: 你应该总是避免synchronized(this)并用一个私人引用上的锁代替它?


一些更多的信息(更新为答案):

  • 我们正在谈论实例同步
  • 同时考虑隐式( synchronized方法)和显式forms的synchronized(this)
  • 如果您引用Bloch或其他权威人士的话,请不要忽略您不喜欢的部分(例如Effective Java,线程安全上的项目: 通常它是实例本身的锁,但也有例外)。
  • 如果你的locking需要粒度,而不是synchronized(this)提供,那么synchronized(this)不适用,所以这不是问题

我会分别介绍每一点。

  1. 一些邪恶的代码可能会偷你的锁(非常stream行这一个,也有一个“意外”的变体)

    我更偶然的担心。 它的含义是,这是对你的类的一部分暴露的接口,应该logging下来。 有时候需要其他代码使用你的锁的能力。 Collections.synchronizedMap (请参阅javadoc)就是如此。

  2. 同一类中的所有同步方法使用完全相同的锁,这会降低吞吐量

    这是过于简单化的想法; 刚刚摆脱synchronized(this)不会解决问题。 适当的吞吐量同步将需要更多的思考。

  3. 你(不必要地)暴露太多的信息

    这是#1的变体。 synchronized(this)是您的界面的一部分。 如果你不想/需要这个暴露,不要这样做。

那么首先应该指出的是:

 public void blah() { synchronized (this) { // do stuff } } 

在语义上等同于:

 public synchronized void blah() { // do stuff } 

这是不使用synchronized(this)一个原因。 你可能会争辩说,你可以在synchronized(this)块周围做些什么。 通常的原因是尽量避免必须进行同步检查,这会导致各种并发问题,特别是双重检查locking问题 ,这个问题只是表明做出相对简单的检查是多么困难线程安全的。

私人锁是一种防御机制,这绝不是一个坏主意。

另外,正如你所暗示的,私人锁可以控制粒度。 一个对象上的一组操作可能与另一个对象完全无关,但synchronized(this)将相互排斥对所有对象的访问。

synchronized(this)只是真的不给你任何东西。

当你使用synchronized(this)时,你正在使用类实例作为一个锁本身。 这意味着当线程1获得locking时,线程2应该等待

假设下面的代码

 public void method1() { do something ... synchronized(this) { a ++; } ................ } public void method2() { do something ... synchronized(this) { b ++; } ................ } 

方法1修改variablesa ,方法2修改variablesb ,应该避免两个线程同时修改同一个variables。 但是,当thread1修改athread2修改b时,它可以在没有任何竞态条件下执行。

不幸的是,上面的代码不会允许这个,因为我们使用相同的引用来locking; 这意味着线程即使不处于竞争状态也应该等待,显然代码牺牲了程序的并发性。

解决方法是对两个不同的variables使用2个不同的锁。

  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个锁而不是1个(分别为variablesab分别为lockAlockB ),结果允许更好的并发性,另一方面它比第一个例子变得更加复杂。

虽然我同意不盲从教条式的规则,但是“locking盗窃”的场景对你来说显得如此古怪吗? 一个线程可以确实获得对象的“外部”locking( synchronized(theObject) {...} ),阻止等待同步实例方法的其他线程。

如果您不相信恶意代码,请考虑此代码可能来自第三方(例如,如果您开发某种应用程序服务器)。

这个“偶然”的版本似乎不大可能,但正如他们所说的,“做出一些愚蠢的事情,有人会发明一个更好的傻子”。

所以我同意这种看法 – 这是什么types的学校。


编辑下面eljenso的前3个评论:

我从来没有经历过盗窃的问题,但这是一个假想的情况:

假设你的系统是一个servlet容器,我们正在考虑的对象是ServletContext实现。 它的getAttribute方法必须是线程安全的,因为上下文属性是共享数据; 所以你声明它是synchronized 。 让我们也想象一下,你提供一个基于你的容器实现的公共托pipe服务。

我是您的客户,并在您的网站上部署我的“良好”的servlet。 碰巧,我的代码包含对getAttribute的调用。

一名伪装成另一名顾客的黑客在他的网站上部署了恶意的servlet。 它在init方法中包含以下代码:

 synchronized(this.getServletConfig()。getServletContext()){
    while(true){}
 }

假设我们共享相同的servlet上下文(只要这两个servlet在同一个虚拟主机上,规范允许),我对getAttribute调用永远被locking。 黑客已经在我的servlet上实现了DoS。

如果getAttribute在私有锁上同步,则此攻击是不可能的,因为第三方代码无法获取此锁。

我承认这个例子是人为devise的,并且是一个servlet容器如何工作的过分简单的观点,但恕我直言,它certificate了这一点。

所以我会根据安全考虑做出我的deviseselect:我将完全控制可以访问实例的代码吗? 线程无限期地locking一个实例的结果是什么?

在C#和Java阵营中,似乎有不同的共识。 我见过的大部分Java代码都使用了:

 // apply mutex to this instance synchronized(this) { // do work here } 

而大多数的C#代码select可以说是更安全的:

 // 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的挖掘,只是反映了我在这两种语言上工作的经验。

java.util.concurrent包大大降低了我的线程安全代码的复杂性。 我只有轶事证据继续下去,但是我看到的大多数synchronized(x)似乎是重新实现一个Lock,Semaphore或Latch,但使用较低级别的监视器。

考虑到这一点,使用任何这些机制进行同步就类似于在内部对象上进行同步,而不是泄漏locking。 这是有利的,因为您可以绝对确定您是通过两个或更多的线程来控制进入监视器的。

如果你已经决定:

  • 你需要做的是locking当前对象; 和
  • 你希望以比整个方法更小的粒度来locking它;

那么我不会看到一个关于synchronizezd(this)的禁忌。

有些人故意在一个方法的全部内容中使用synchronized(this)(而不是标记方法synchronized),因为他们认为哪个对象实际上正在被同步到“读者更清楚”。 只要人们做出明智的select(例如,明白这样做,他们实际上是将额外的字节码插入到方法中,这可能对潜在的优化有一个连锁效应),但我并不特别看到这个问题。 你应该总是logging你的程序的并发行为,所以我没有看到“同步”发布的行为“论据如此引人注目。

至于你应该使用哪个对象的锁的问题,我认为如果你正在做什么以及你的类将如何被使用 ,那么当前对象的同步没有任何问题。 例如,对于集合,逻辑上期望locking的对象通常是集合本身。

  1. 如果可能的话,使你的数据不可变( finalvariables)
  2. 如果你不能避免在多个线程之间共享数据的变异,使用高级编程结构[例如粒度Lock API]

锁提供对共享资源的独占访问权限:一次只有一个线程可以获取该锁,并且对共享资源的所有访问都要求首先获取该锁。

示例代码使用实现Lock接口的ReentrantLock

  class X { private final ReentrantLock lock = new ReentrantLock(); // ... public void m() { lock.lock(); // block until condition holds try { // ... method body } finally { lock.unlock() } } } 

locking同步的优点(this)

  1. 同步方法或语句的使用强制所有的锁获取和释放以块结构的方式进行。

  2. locking实现提供了使用同步方法和语句的附加function

    1. 获取锁的非阻塞尝试( tryLock()
    2. 尝试获取可以中断的锁( lockInterruptibly()
    3. 尝试获取可能超时的锁( tryLock(long, TimeUnit) )。
  3. 一个Lock类也可以提供与隐式监视器锁相当不同的行为和语义,比如

    1. 保证订货
    2. 不可重复使用
    3. 死锁检测

看看这个SE关于各种types的Locks

同步与locking

您可以通过使用高级并发API而不是同步块来实现线程安全。 这个文档页面提供了很好的编程结构来实现线程安全。

锁对象支持简化许多并发应用程序的locking习惯用法。

执行者定义了一个用于启动和pipe理线程的高级API。 由java.util.concurrent提供的执行器实现提供了适用于大规模应用程序的线程池pipe理。

并发集合可以更容易地pipe理大量的数据集合,并且可以大大减less同步的需要。

primefacesvariables具有最小化同步的function,有助于避免内存一致性错误。

ThreadLocalRandom (在JDK 7中)提供了从多个线程高效地生成伪随机数。

对于其他编程构造,也参考java.util.concurrent和java.util.concurrent.atomic包。

这取决于情况。
如果只有一个共享实体或多个共享实体。

看到完整的工作示例

一个小介绍。

线程和可共享的实体
multithreading访问同一个实体是可能的,例如多个connectionThreads共享一个messageQueue。 由于线程并发运行,有可能会被另一个数据覆盖另一个数据,这可能是一个混乱的情况。
所以我们需要一些方法来确保共享实体一次只能被一个线程访问(CONCURRENCY)。

同步块
synchronized()块是确保可共享实体并发访问的一种方法。
首先是一个小的比喻
假设洗手间内有两个人P1,P2(线程)洗脸盆(可共享实体),并有门(锁)。
现在我们要一个人一次使用脸盆。
方法是通过P1锁门,当门锁上时,P2等待,直到p1完成他的工作
P1打开门
那么只有P1可以使用洗脸盆。

句法。

 synchronized(this) { SHARED_ENTITY..... } 

“this”提供了与类相关的内部锁(Java开发人员devise的Object类,使得每个对象都可以像监视器一样工作)。 当只有一个共享实体和多个线程(1:N)时,上述方法正常工作。
在这里输入图像描述 N个可共享的实体 – M个线程
现在想想洗手间里只有一个门的时候有两个脸盆的情况。 如果我们使用以前的方法,只有p1可以一次使用一个洗脸盆,而p2将在外面等待。 没有人使用B2(洗脸盆),这是浪费资源。
更明智的做法是在洗手间内创build一个较小的房间,并为每个洗脸盆提供一个门。 通过这种方式,P1可以访问B1,P2可以访问B2,反之亦然。

 washbasin1; washbasin2; Object lock1=new Object(); Object lock2=new Object(); synchronized(lock1) { washbasin1; } synchronized(lock2) { washbasin2; } 

在这里输入图像描述
在这里输入图像描述

请参阅线程 —-> 这里

不,你不应该永远 。 但是,当一个特定的对象需要多个线程安全时,我倾向于避免这个问题。 例如,您可能有一个具有“标签”和“父”字段的可变数据对象; 这些需要是线程安全的,但改变一个不需要阻止另一个被写入/读取。 (实际上,我会通过声明字段volatile和/或使用java.util.concurrent的AtomicFoo包装来避免这种情况。

一般来说,同步是有点笨拙的,因为它抨击了一个大的锁,而不是准确地思考如何允许线程彼此工作。 使用synchronized(this)是更加笨拙和反社会的,因为它说:“当我locking的时候,没有人可以改变这个类的任何东西 ”。 你多久需要这样做?

我宁愿有更多的粒状锁; 即使你想阻止所有的事情发生改变(也许你在序列化对象),你可以获得所有的锁来实现同样的事情,再加上它更加明确。 当你使用synchronized(this) ,不清楚你为什么要同步,或者是什么副作用。 如果你使用synchronized(labelMonitor) ,或者甚至更好的labelLock.getWriteLock().lock() ,你很清楚你在做什么以及你的关键部分的作用是有限的。

简短的回答 :你必须了解差异,并根据代码进行select。

长的回答 :总的来说,我宁愿尽量避免同步(这)来减less争用,但私人锁增加复杂性,你必须注意。 所以使用正确的同步进行正确的工作。 如果你对multithreading编程不太熟练,我宁愿坚持实例locking,并阅读这个主题。 (这就是说:只使用同步(这个)不会自动使你的类完全线程安全)。这不是一个容易的话题,但是一旦你习惯了,是否使用同步(this)或不使用。

我认为在Brian Goetz的一本名为Java Concurrency In Practice的书中,为什么这些都是重要的技术。 他有一点非常清楚 – 你必须使用“无处不在”的锁来保护你的对象的状态。 同步的方法和对象上的同步往往是并行的。 例如Vector同步所有的方法。 如果你有一个vector对象的句柄,并要做“如果不存在”,那么只有Vector同步它自己的单独的方法不会保护你免受国家的腐败。 您需要使用同步(vectorHandle)进行同步。 这将导致SAME锁被每个具有向量句柄的线程获取,并将保护向量的整体状态。 这被称为客户端locking。 事实上,我们知道向量确实同步(this)/同步它的所有方法,因此在对象上同步vectorHandle将导致向量对象状态的正确同步。 它愚蠢的相信,你是线程安全的,只是因为你正在使用线程安全的集合。 这正是ConcurrentHashMap明确引入putIfAbsent方法的原因 – 使这种操作成为primefaces。

综上所述

  1. 在方法级同步允许客户端locking。
  2. 如果你有一个私人锁对象 – 这使得客户端locking不可能。 如果你知道你的类没有“如果不存在”types的function,这很好。
  3. 如果你正在devise一个库 – 那么同步或者同步这个方法通常是比较明智​​的。 因为你很less能够决定你的class级将如何使用。
  4. 如果Vector使用了一个私人locking对象 – 如果没有这个对象,就不可能得到“放置”的权利。 客户端代码永远不会获得私有锁的处理,从而破坏了使用“完全相同锁”来保护其状态的基本规则。
  5. 同步这个或同步的方法确实有一个问题,正如其他人指出的 – 有人可以得到一个锁,而不会释放它。 所有其他线程将继续等待锁释放。
  6. 所以知道你在做什么,并采取正确的。
  7. 有人认为拥有一个私有锁对象可以提供更好的粒度 – 例如,如果两个操作是不相关的 – 可以通过不同的锁来保护,从而获得更好的吞吐量。 但是,我认为这是devise的气味,而不是代码气味 – 如果两个操作是完全不相关的,他们为什么是SAME类的一部分? 为什么class级俱乐部不相关的function呢? 可能是一个实用课程? 嗯 – 一些util通过相同的实例提供string操作和日历date格式? 至less对我来说没有任何意义!

锁被用于可见性或用于保护一些数据免受可能导致竞争的并发修改

当你需要将原始types操作设为primefaces时,可以使用AtomicInteger等类似的选项。

但假设你有两个彼此相关的整数,如xy坐标,它们是相互关联的,应该以primefaces方式改变。 那么你会用同样的锁来保护他们。

锁只能保护相互关联的状态。 没有更less,也没有更多。 如果在每个方法中使用synchronized(this) ,则即使类的状态是不相关的,即使更新不相关的状态,所有线程也将面临争用。

 class Point{ private int x; private int y; public Point(int x, int y){ this.x = x; this.y = y; } //mutating methods should be guarded by same lock public synchronized void changeCoordinates(int x, int y){ this.x = x; this.y = y; } } 

在上面的例子中,我只有一个方法可以改变xy而不是两个不同的方法,因为xy是相关的,如果我给出了两个不同的方法分别对xy进行变异,那么它就不是线程安全的。

这个例子只是为了演示,而不一定是应该实施的方式。 做到这一点的最好方法是使其成为不可变的

现在与Point例子相反,有一个由@Andreas提供的TwoCounters的例子,其中两个不同的锁作为状态被保护的状态是彼此无关的。

使用不同的锁来保护不相关的状态的过程被称为locking条带化或locking分裂

不同步的原因是,有时你需要多于一个的锁(第二个锁通常在经过一些额外的思考之后被移除,但是你仍然需要它在中间状态)。 如果你locking这个 ,你总是要记住两个锁中的哪一个是这个 ; 如果你locking一个私人对象,variables名告诉你。

从读者的angular度来看,如果你看到这个问题 ,你总是必须回答两个问题:

  1. 什么样的访问受到这个保护?
  2. 是一个真正的锁,没有人引入一个错误?

一个例子:

 class BadObject { private Something mStuff; synchronized setStuff(Something stuff) { mStuff = stuff; } synchronized getStuff(Something stuff) { return mStuff; } private MyListener myListener = new MyListener() { public void onMyEvent(...) { setStuff(...); } } synchronized void longOperation(MyListener l) { ... l.onMyEvent(...); ... } } 

如果两个线程在两个不同的BadObject实例上开始longOperation() ,它们获取它们的锁; 当调用l.onMyEvent(...) ,我们有一个死锁,因为这两个线程都不会获得另一个对象的锁。

在这个例子中,我们可以通过使用两个锁来消除死锁,一个用于短操作,另一个用于长操作。

正如已经说过的那样,同步块可以使用用户定义的variables作为锁对象,当同步函数只使用“this”的时候。 当然,你可以操作你的函数应该同步的区域等等。

但是大家都说使用“this”作为锁对象的同步函数和覆盖整个函数的block没有区别。 这是不正确的,区别在于两种情况下都会产生的字节码。 在同步块使用的情况下,应该分配引用“this”的局部variables。 因此,我们将有一个更大的function大小(不相关,如果你只有less数function)。

更详细的解释你可以在这里find差异: http : //www.artima.com/insidejvm/ed2/threadsynchP.html

由于以下观点,同步块的使用也不好:

synchronized关键字在一个区域非常有限:当退出同步块时,所有等待该锁的线程都必须被解除阻塞,但只有其中一个线程可以取锁; 所有其他人都看到了锁被带走并回到被阻止的状态。 这不仅浪费了大量的处理周期:通常上下文切换到解除阻塞线程还涉及从磁盘分页内存,这非常非常昂贵。

有关这方面的更多细节,我build议你阅读这篇文章: http : //java.dzone.com/articles/synchronized-considered

使用synchronized(this)的一个很好的例子。

 // add listener public final synchronized void addListener(IListener l) {listeners.add(l);} // remove listener public final synchronized void removeListener(IListener l) {listeners.remove(l);} // routine that raise events public void run() { // some code here... Set ls; synchronized(this) { ls = listeners.clone(); } for (IListener l : ls) { l.processEvent(event); } // some code here... } 

正如你在这里可以看到的,我们在这里使用同步方法来容易地配合(可能无限循环的方法)和一些同步的方法。

Of course it can be very easily rewritten with using synchronized on private field. But sometimes, when we already have some design with synchronized methods (ie legacy class, we derive from, synchronized(this) can be the only solution).

It depends on the task you want to do, but I wouldn't use it. Also, check if the thread-save-ness you want to accompish couldn't be done by synchronize(this) in the first place? There are also some nice locks in the API that might help you 🙂

I think points one (somebody else using your lock) and two (all methods using the same lock needlessly) can happen in any fairly large application. Especially when there's no good communication between developers.

It's not cast in stone, it's mostly an issue of good practice and preventing errors.