Avoid synchronized(this) in Java?
每当一个问题出现在Java同步上时,有些人非常急切地指出应该避免EDCOX1 0。相反,他们声称,最好锁定一个私有引用。
其中一些原因是:
- 一些邪恶的密码可能会偷走你的锁(这个很流行,也有一个"意外"的变体)
- 同一类中的所有同步方法都使用完全相同的锁,这会降低吞吐量
- 你(不必要地)暴露了太多的信息
其他人,包括我,认为EDOCX1 0是一个惯用的习惯用法(也在爪哇的图书馆里),是安全和很好理解的。这是不应该避免的,因为你有一个错误,你不知道在你的多线程程序中发生了什么。换句话说:如果它适用,那么就使用它。
我有兴趣看到一些现实世界中的例子(没有foobar的东西),当
因此:您是否应该总是避免使用
更多信息(更新为答案):
- 我们讨论的是实例同步
- 同时考虑了隐式(
synchronized 方法)和显式形式的synchronized(this) 。 - 如果你引用布洛赫或其他有关这个问题的权威,不要漏掉你不喜欢的部分(例如有效的Java,线程安全的项目:通常它是实例本身的锁,但也有例外)。
- 如果您的锁定中需要粒度而不是
synchronized(this) 提供,那么synchronized(this) 不适用,因此这不是问题所在。
我将分别讨论每一点。
Some evil code may steal your lock (very popular this one, also has an
"accidentally" variant)
我更担心意外。这意味着使用
All synchronized methods within the same class use the exact same
lock, which reduces throughput
这是过于简单化的想法;仅仅摆脱
You are (unnecessarily) exposing too much information
这是1的变体。使用
首先应该指出:
1 2 3 4 5 | public void blah() { synchronized (this) { // do stuff } } |
在语义上等价于:
1 2 3 | public synchronized void blah() { // do stuff } |
这是不使用
私有锁是一种防御机制,这绝不是一个坏主意。
另外,正如您所提到的,私有锁可以控制粒度。一个对象上的一组操作可能与另一个完全无关,但
使用synchronized(this)时,将类实例用作锁本身。这意味着当线程1获取锁时,线程2应该等待
假设以下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | public void method1() { do something ... synchronized(this) { a ++; } ................ } public void method2() { do something ... synchronized(this) { b ++; } ................ } |
方法1修改变量A,方法2修改变量B,避免两个线程同时修改同一个变量。但是当thread1修改a和thread2修改b时,可以在没有任何竞争条件的情况下执行。
不幸的是,上面的代码不允许这样做,因为我们对一个锁使用相同的引用;这意味着线程即使不在争用条件下也应该等待,显然代码会牺牲程序的并发性。
解决方案是对两个不同的变量使用两个不同的锁。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
上面的示例使用了更多的细粒度锁(2个锁而不是一个(变量a和b分别使用locka和lockb),因此允许更好的并发性,另一方面,它变得比第一个示例更复杂…
虽然我同意不要盲目地遵守教条,但"偷锁"的情况对你来说是否如此古怪?线程确实可以"从外部"获取对象的锁(
如果您不相信恶意代码,请考虑该代码可能来自第三方(例如,如果您开发某种应用程序服务器)。
"偶然"的版本似乎不太可能,但正如他们所说,"做一些白痴证明,有人会发明一个更好的白痴"。
所以我同意这一点,这取决于学校的思想是什么。
编辑以下Eljenso的前3条评论:
我从来没有遇到过偷锁的问题,但这里有一个假想的场景:
假设您的系统是一个servlet容器,我们考虑的对象是
我是你的客户,在你的网站上部署我的"好"servlet。碰巧我的代码包含一个对
一个黑客伪装成另一个客户,在你的网站上部署他的恶意servlet。它在
1 2 3 | synchronized (this.getServletConfig().getServletContext()) { while (true) {} } |
假设我们共享相同的servlet上下文(只要两个servlet在同一虚拟主机上,规范就允许),那么我对
如果
我承认这个例子是人为的,对servlet容器的工作方式有一个过于简单的观点,但我认为它证明了这一点。
因此,我将根据安全性考虑做出设计选择:我是否可以完全控制可以访问实例的代码?线程无限期地持有实例上的锁会导致什么后果?
在C和JAVA营地上似乎有不同的共识。我见过的大多数Java代码使用:
1 2 3 4 | // apply mutex to this instance synchronized(this) { // do work here } |
尽管大多数C代码选择了可以说更安全的代码:
1 2 3 4 5 6 7 8 9 10 | // 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的挖潜,只是我对这两种语言的体验的反映。
这取决于情况。如果只有一个或多个共享实体。
请参阅这里的完整工作示例
小小的介绍。
线程和可共享实体多个线程可以访问同一个实体,例如共享单个消息队列的多个连接线程。由于线程同时运行,可能会有机会用另一个线程覆盖一个人的数据,这可能是一个混乱的情况。因此,我们需要某种方法来确保可共享实体一次只能由一个线程访问。(并发)。
同步块synchronized()块是确保可共享实体并发访问的一种方法。首先,一个小的类比假设有两个人,p1,p2(线程),一个洗脸盆(共享实体),在一个卫生间里,有一扇门(锁)。现在我们要一个人一次使用脸盆。一种方法是在门被锁定时通过p1锁定门,p2等待p1完成他的工作。P1打开门只有P1可以使用洗脸盆。
语法。
1 2 3 4 | synchronized(this) { SHARED_ENTITY..... } |
"这个"提供了与类相关联的内在锁(Java开发人员设计的对象类,这样每个对象可以作为监视器工作)。当只有一个共享实体和多个线程(1:n)时,上述方法可以很好地工作。n个可共享实体-m个线程现在想想这样一种情况:一个洗手间里有两个洗脸盆,只有一扇门。如果我们使用前面的方法,一次只有p1可以使用一个洗脸盆,而p2将在外面等待。这是资源浪费,因为没有人使用B2(洗脸盆)。一个更明智的方法是在洗手间内创建一个较小的房间,并为每个洗脸盆提供一个门。这样,p1可以访问b1,p2可以访问b2,反之亦然。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
查看更多关于线程的信息---->这里
考虑到这一点,使用这些机制中的任何一种进行同步都类似于在内部对象上进行同步,而不是泄漏锁。这是有益的,因为您可以绝对确定通过两个或多个线程控制进入监视器的条目。
A Lock provides exclusive access to a shared resource: only one thread at a time can acquire the lock and all access to the shared resource requires that the lock be acquired first.
使用
1 2 3 4 5 6 7 8 9 10 11 12 13 | class X { private final ReentrantLock lock = new ReentrantLock(); // ... public void m() { lock.lock(); // block until condition holds try { // ... method body } finally { lock.unlock() } } } |
同步锁定的优点(此)
使用同步方法或语句会强制以块结构方式进行所有锁的获取和释放。
锁实现通过提供
锁类还可以提供与隐式监视器锁截然不同的行为和语义,例如
请看一下关于各种类型的
同步与锁定
您可以通过使用高级并发API而不是同步块来实现线程安全。此文档页提供了良好的编程结构,以实现线程安全。
锁定对象支持简化许多并发应用程序的锁定习惯用法。
执行器定义用于启动和管理线程的高级API。由java.util.concurrent提供的执行器实现提供了适用于大型应用程序的线程池管理。
并发集合使管理大型数据集合变得更容易,并且可以大大减少同步的需要。
原子变量具有最小化同步和帮助避免内存一致性错误的功能。
threadLocalRandom(在JDK7中)提供了从多个线程高效生成伪随机数的功能。
有关其他编程构造,请参阅java.util.concurrent和java.util.concurrent.atomic包。
如果你决定:
- 你需要做的是锁定当前对象;以及
- 你想锁定粒度小于整体方法;
那么,我不认为同步化的禁忌(这个)。
有些人故意在一个方法的整个内容中使用synchronized(this)(而不是将方法标记为synchronized),因为他们认为它"对读者来说更清楚"哪个对象实际上正在被同步。只要人们做出一个明智的选择(例如,通过这样做,他们实际上是在向方法中插入额外的字节码,这可能会对潜在的优化产生连锁反应),我不会特别看到这个问题。您应该始终记录程序的并发行为,所以我不认为"synchronized"发布行为"参数如此引人注目。
关于您应该使用哪个对象的锁的问题,我认为在当前对象上进行同步没有什么错,如果您正在做什么以及类通常如何使用的逻辑会期望这样做。例如,对于集合,逻辑上期望锁定的对象通常是集合本身。
我认为有一个很好的解释,为什么在Brian Goetz的实践中,这些都是你的腰带中的关键技术。他有一点非常清楚——你必须在"任何地方"使用相同的锁来保护你的物体的状态。同步方法和对象上的同步经常是同时进行的。例如,矢量同步其所有方法。如果你有一个向量对象的处理方法,并且打算做"如果不存在就放",那么仅仅同步向量本身的方法并不能保护你免受状态的破坏。您需要使用同步(vectorhandle)进行同步。这将导致具有向量句柄的每个线程获取相同的锁,并保护向量的整体状态。这称为客户端锁定。我们知道,事实上,矢量是同步的(这个)/同步所有的方法,因此在对象矢量手柄上同步将导致矢量对象状态的适当同步。仅仅因为使用的是线程安全集合,就认为您是线程安全的,这是愚蠢的。这正是Concurrenthashmap明确引入putifastent方法的原因——使这种操作具有原子性。
综上所述
简短的回答:你必须了解区别,并根据代码做出选择。
长话短说:一般来说,我宁愿尝试避免同步(这个)来减少争用,但是私有锁增加了您必须注意的复杂性。因此,对正确的作业使用正确的同步。如果您对多线程编程没有那么丰富的经验,我宁愿坚持实例锁定并阅读本主题。(这就是说:仅仅使用synchronize(this)并不能自动使类完全线程安全。)这不是一个简单的主题,但是一旦您习惯了它,是否使用synchronize(this)的答案就会自然出现。
不,你不应该总是这样。但是,当一个特定对象上存在多个关注点时,我倾向于避免这种情况,因为这些关注点只需要对其自身进行线程安全即可。例如,您可能有一个具有"label"和"parent"字段的可变数据对象;这些字段必须是threadsafe,但是更改一个字段不需要阻止另一个对象被写入/读取。(在实践中,我会通过声明字段不稳定和/或使用java. U.L.Read的AtomicFoo包装器来避免这种情况)。
一般来说,同步有点笨拙,因为它触发了一个很大的锁,而不是精确地考虑线程如何被允许在彼此之间工作。使用EDOCX1[0]甚至更加笨拙和反社会,因为它的意思是"当我持有锁的时候,没有人可以改变这个班上的任何东西"。你需要多久做一次?
我更愿意拥有更细粒度的锁;即使你真的想阻止所有的事情发生变化(也许你正在序列化对象),你也可以获取所有的锁来实现相同的事情,而且这样做更为明确。当你使用
锁用于可见性或保护某些数据不受可能导致争用的并发修改的影响。
当您只需要将原始类型操作设置为原子操作时,可以使用诸如
但是假设你有两个相互关联的整数,比如
锁应该只保护彼此相关的状态。不少也不多。如果在每个方法中使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
在上面的示例中,我只有一种方法可以同时对
这个例子只是为了演示而不一定是应该实现的方式。最好的方法是使它不可变。
现在,与
使用不同的锁来保护不相关状态的过程称为锁条带化或锁拆分。
不同步的原因是有时需要一个以上的锁(第二个锁经常在经过一些额外的思考后被移除,但是您仍然需要处于中间状态)。如果你锁定了它,你总是要记住这两个锁中的哪一个是这个;如果你锁定了一个私有对象,变量名会告诉你这一点。
从读者的角度来看,如果你看到锁定这一点,你总是需要回答以下两个问题:
一个例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | 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(...); ... } } |
如果两个线程在两个不同的
在这个例子中,我们可以使用两个锁来消除死锁,一个用于短操作,另一个用于长操作。
如前所述,当同步函数只使用"this"时,同步块可以使用用户定义的变量作为锁对象。当然,您可以对应该同步的函数区域进行操作,等等。
但大家都说,使用"this"作为锁对象覆盖整个函数的synchronized函数和block没有区别。这不是真的,差异在于两种情况下都会生成的字节代码。在同步块的情况下,应分配局部变量,该变量保存对"this"的引用。因此,我们将有一个稍微大一点的函数大小(如果您只有几个函数,则与此无关)。
您可以在这里找到关于差异的更详细的解释:http://www.artima.com/insidejvm/ed2/threadsynchp.html
同时,由于以下观点,同步块的使用也不好:
The synchronized keyword is very limited in one area: when exiting a synchronized block, all threads that are waiting for that lock must be unblocked, but only one of those threads gets to take the lock; all the others see that the lock is taken and go back to the blocked state. That's not just a lot of wasted processing cycles: often the context switch to unblock a thread also involves paging memory off the disk, and that's very, very, expensive.
有关此领域的更多详细信息,我建议您阅读本文:http://java.dzone.com/articles/synchronized-considered
这实际上只是对其他答案的补充,但是如果您对使用私有对象进行锁定的主要反对意见是它使类中的字段与业务逻辑无关,那么项目lombok让
1 2 3 4 | @Synchronized public int foo() { return 0; } |
编译到
1 2 3 4 5 6 7 |
避免使用
1 2 3 4 5 6 7 8 9 | public void foo() { if(operation = null) { synchronized(foo) { if (operation == null) { // enter your code that this method has to handle... } } } } |
这取决于你想做的任务,但我不会用它。另外,首先通过同步(这个)来检查您想要伴随的线程保存是否不能完成?API中还有一些很好的锁可以帮助您:)
我只想提一个在没有依赖关系的代码的原子部分中唯一私有引用的可能解决方案。可以使用带有锁的静态哈希映射和名为atomic()的简单静态方法,该方法使用堆栈信息(完整类名和行号)自动创建所需的引用。然后,可以在同步语句中使用此方法,而无需编写新的锁对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | // Synchronization objects (locks) private static HashMap<String, Object> locks = new HashMap<String, Object>(); // Simple method private static Object atomic() { StackTraceElement [] stack = Thread.currentThread().getStackTrace(); // get execution point StackTraceElement exepoint = stack[2]; // creates unique key from class name and line number using execution point String key = String.format("%s#%d", exepoint.getClassName(), exepoint.getLineNumber()); Object lock = locks.get(key); // use old or create new lock if (lock == null) { lock = new Object(); locks.put(key, lock); } return lock; // return reference to lock } // Synchronized code void dosomething1() { // start commands synchronized (atomic()) { // atomic commands 1 ... } // other command } // Synchronized code void dosomething2() { // start commands synchronized (atomic()) { // atomic commands 2 ... } // other command } |
使用synchronized(this)的一个好例子。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // 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... } |
正如您在这里看到的,我们在这里使用synchronize,以便与那里的一些同步方法进行长时间的合作(可能是无限循环的run方法)。
当然,在私有字段上使用synchronized可以很容易地重写它。但有时,当我们已经有了一些使用同步方法的设计(例如,遗留类,我们从中派生,同步(这个)可能是唯一的解决方案)。
我认为,在任何相当大的应用程序中,都可能出现一个(其他人使用您的锁)和两个(所有方法都不必要地使用同一个锁)。尤其是当开发人员之间没有良好的沟通时。
它不是石头铸成的,它主要是一个良好实践和防止错误的问题。