每当在SO上出现关于Java同步的问题时,有些人都急于指出应该避免同步(这一点)。相反,他们声称,对私有引用的锁定是首选的。
其中一些原因是:
一些邪恶的代码可能会偷你的锁(这个很流行,也有一个“意外”变体)
同一类中的所有同步方法都使用完全相同的锁,这降低了吞吐量
你(不必要地)暴露了太多信息
包括我在内的其他人认为,synchronized(this)是一个被大量使用的习语(在Java库中也是如此),是安全的,而且很容易理解。它不应该被避免,因为你有一个错误,你不知道你的多线程程序中发生了什么。换句话说:如果它是适用的,那么就使用它。
我感兴趣的是看到一些现实世界的例子(没有foobar的东西),避免锁定在这是可取的,当同步(这)也会做的工作。
因此:您应该总是避免synchronized(this),并将其替换为私有引用上的锁吗?
一些进一步的信息(随着答案的更新):
we are talking about instance synchronization
both implicit (synchronized methods) and explicit form of synchronized(this) are considered
if you quote Bloch or other authorities on the subject, don't leave out the parts you don't like (e.g. Effective Java, item on Thread Safety: Typically it is the lock on the instance itself, but there are exceptions.)
if you need granularity in your locking other than synchronized(this) provides, then synchronized(this) is not applicable so that's not the issue
锁可以用于可见性,也可以用于保护一些数据不受可能导致竞争的并发修改的影响。
当您需要将基本类型操作设置为原子类型时,可以使用AtomicInteger之类的选项。
但是假设你有两个整数,它们像x和y坐标一样彼此相关,它们彼此相关,应该以原子的方式改变。然后使用相同的锁来保护它们。
锁应该只保护彼此相关的状态。不多不少。如果在每个方法中都使用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;
}
}
在上面的例子中,我只有一个方法同时改变x和y,而不是两个不同的方法,因为x和y是相关的,如果我给了两个不同的方法分别改变x和y,那么它就不会是线程安全的。
这个例子只是为了演示它的实现方式,而不一定是这样。最好的方法是让它成为IMMUTABLE。
现在,与Point例子相反的是,@Andreas已经提供了一个TwoCounters的例子,其中状态被两个不同的锁保护,因为状态彼此不相关。
使用不同的锁来保护不相关的状态的过程称为锁剥离或锁分裂
这里已经说过,同步块可以使用用户定义的变量作为锁对象,当同步函数只使用“this”时。当然,你也可以对函数中需要同步的部分进行操作。
但是每个人都说synchronized函数和block之间没有区别,block覆盖了使用“this”作为锁对象的整个函数。这是不对的,不同的是字节码,将在这两种情况下产生。在同步块使用的情况下,应该分配本地变量,其中包含引用“this”。因此,我们会得到一个更大的函数(如果你只有几个函数,这就无关紧要了)。
你可以在这里找到更详细的解释:
http://www.artima.com/insidejvm/ed2/threadsynchP.html
同步块的使用也不好,原因如下:
synchronized关键字在一个方面非常有限:当退出一个同步块时,所有等待该锁的线程都必须被解除阻塞,但只有其中一个线程可以获得锁;所有其他人都看到锁已被占用,并返回阻塞状态。这不仅仅是浪费了大量的处理周期:为解除线程阻塞而进行的上下文切换通常还涉及从磁盘调出内存,这是非常非常昂贵的。
关于这方面的更多细节,我建议你阅读这篇文章:
http://java.dzone.com/articles/synchronized-considered
如果你已经决定:
你要做的就是锁定目标
当前对象;而且
你想要
锁定粒度小于
整体方法;
那么我就不认为synchronizezd是一个禁忌。
Some people deliberately use synchronized(this) (instead of marking the method synchronized) inside the whole contents of a method because they think it's "clearer to the reader" which object is actually being synchronized on. So long as people are making an informed choice (e.g. understand that by doing so they're actually inserting extra bytecodes into the method and this could have a knock-on effect on potential optimisations), I don't particularly see a problem with this. You should always document the concurrent behaviour of your program, so I don't see the "'synchronized' publishes the behaviour" argument as being so compelling.
至于应该使用哪个对象的锁的问题,我认为在当前对象上同步并没有什么错,如果这是你所做的逻辑所期望的,以及你的类通常是如何被使用的。例如,对于集合,逻辑上期望锁定的对象通常是集合本身。
虽然我同意不要盲目地遵守教条规则,但“偷锁”的场景对你来说是不是很古怪?一个线程确实可以从你的对象“外部”获得锁(synchronized(theObject){…}),阻塞其他线程等待同步实例方法。
如果您不相信恶意代码,请考虑这些代码可能来自第三方(例如,如果您开发了某种应用程序服务器)。
“意外”版本似乎不太可能,但就像他们说的那样,“让一些东西不受白痴的影响,就会有人发明一个更好的白痴”。
所以我同意“这取决于这个班级做什么”的观点。
编辑以下eljenso的前3条评论:
我从来没有遇到过偷锁的问题,但这里有一个想象的场景:
假设您的系统是一个servlet容器,我们考虑的对象是ServletContext实现。它的getAttribute方法必须是线程安全的,因为上下文属性是共享数据;所以你声明它是同步的。让我们再想象一下,您提供了一个基于容器实现的公共托管服务。
我是您的客户,并在您的站点上部署我的“好”servlet。我的代码碰巧包含对getAttribute的调用。
黑客伪装成另一个客户,在您的站点上部署恶意servlet。它在init方法中包含以下代码:
synchronized (this.getServletConfig().getServletContext()) {
while (true) {}
}
假设我们共享相同的servlet上下文(只要两个servlet位于同一个虚拟主机上,规范就允许),那么我对getAttribute的调用将永远锁定。黑客已经在我的servlet上实现了DoS。
如果getAttribute在私有锁上同步,则这种攻击是不可能的,因为第三方代码无法获得此锁。
我承认这个例子是人为设计的,对servlet容器如何工作的看法过于简单,但恕我直言,它证明了这一点。
因此,我将基于安全性考虑做出设计选择:我是否能够完全控制访问实例的代码?线程无限期地持有实例锁的后果是什么?
锁可以用于可见性,也可以用于保护一些数据不受可能导致竞争的并发修改的影响。
当您需要将基本类型操作设置为原子类型时,可以使用AtomicInteger之类的选项。
但是假设你有两个整数,它们像x和y坐标一样彼此相关,它们彼此相关,应该以原子的方式改变。然后使用相同的锁来保护它们。
锁应该只保护彼此相关的状态。不多不少。如果在每个方法中都使用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;
}
}
在上面的例子中,我只有一个方法同时改变x和y,而不是两个不同的方法,因为x和y是相关的,如果我给了两个不同的方法分别改变x和y,那么它就不会是线程安全的。
这个例子只是为了演示它的实现方式,而不一定是这样。最好的方法是让它成为IMMUTABLE。
现在,与Point例子相反的是,@Andreas已经提供了一个TwoCounters的例子,其中状态被两个不同的锁保护,因为状态彼此不相关。
使用不同的锁来保护不相关的状态的过程称为锁剥离或锁分裂
我对2019年的看法,尽管这个问题本可以已经解决了。
如果你知道你在做什么,锁定'this'并不坏,但在幕后锁定'this'是(不幸的是synchronized关键字在方法定义中允许)。
如果你真的希望你的类的用户能够“窃取”你的锁(即阻止其他线程处理它),你实际上希望所有同步方法在另一个同步方法运行时等待,以此类推。
它应该是有意的、经过深思熟虑的(因此有文档来帮助用户理解它)。
更详细地说,反过来,你必须知道如果你锁定了一个不可访问的锁(没有人可以“偷”你的锁,你完全控制等等),你会“获得”(或“失去”)什么。
对我来说,问题是方法定义签名中的synchronized关键字使程序员很容易不考虑锁定什么,如果你不想在多线程程序中遇到问题,这是一个非常重要的事情。
人们不能争辩说,“通常”你不希望你的类的用户能够做这些事情,或者“通常”你想要……这取决于你编写的是什么功能。因为你不能预测所有的用例,所以你不能制定一个经验法则。
例如,prinwriter使用了一个内部锁,但是如果人们不想让他们的输出相互交错,他们就很难从多个线程中使用它。
锁是否可以在类外部访问是程序员根据类的功能来决定的。它是api的一部分。例如,你不能从synchronized(this)移到synchronized(provateObjet)而不冒破坏使用它的代码更改的风险。
注1:我知道你可以实现任何同步(这个)'实现'通过使用显式锁对象和暴露它,但我认为这是不必要的,如果你的行为是很好的记录,你实际上知道什么锁定'this'的意思。
注2:我不同意这样的观点:如果一些代码不小心偷了你的锁,那就是一个bug,你必须解决它。这在某种程度上等同于说我可以让我所有的方法公开,即使它们本不应该是公开的。如果有人“意外”调用我的意图是私人方法,这是一个bug。为什么会发生这样的事故!!如果能偷你的锁对你的类来说是一个问题,那就不要允许它。就这么简单。