避免在Java中同步(这)?

每当在SO上出现关于Java同步的问题时,有些人非常渴望指出synchronized(this)应该避免。相反,他们声称,对私有引用的锁定是首选的。

其中一些原因是:

其他人,包括我,认为synchronized(this)是一个被大量使用的习语(也在Java库中),是安全和易于理解的。它不应该被避免,因为你有一个错误,你不知道你的多线程程序中发生了什么。换句话说:如果它是适用的,那么就使用它。

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

因此:你应该总是避免synchronized(this)并将其替换为私有引用上的锁吗?


一些进一步的信息(随着答案的更新):

  • 我们正在讨论实例同步
  • synchronized(this)的隐式(synchronized方法)和显式形式都被考虑
  • 如果你引用Bloch或其他权威人士的话,不要省略你不喜欢的部分(例如:Effective Java, item on Thread Safety: 通常是实例本身的锁,但也有例外。)
  • 如果你需要的锁定粒度不是synchronized(this)提供的,那么synchronized(this)是不适用的,所以这不是问题所在
152690 次浏览

首先需要指出的是:

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

语义上等价于:

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

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

私有锁是一种防御机制,这从来都不是一个坏主意。

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

synchronized(this)真的什么都没有给你。

我认为第一点(其他人使用您的锁)和第二点(所有方法不必要地使用相同的锁)可能发生在任何相当大的应用程序中。特别是当开发人员之间没有良好的沟通时。

这不是一成不变的,这主要是一个良好的实践和防止错误的问题。

不,你不应该总是。但是,当一个特定对象上有多个关注点时,我倾向于避免它,而这些关注点只需要对它们本身是线程安全的。例如,你可能有一个可变数据对象,它有“label”和“parent”字段;它们需要是线程安全的,但是改变其中一个不需要阻止另一个被写入/读取。(在实践中,我将通过声明字段为volatile和/或使用java.util来避免这种情况。concurrent的AtomicFoo包装器)。

一般来说,同步有点笨拙,因为它只是一个大的锁定,而不是仔细考虑如何允许线程相互工作。使用synchronized(this)甚至更笨拙和反社会,因为它说“当我持有锁时,没有人可以改变这个类的任何东西”。你需要多久做一次?

我宁愿拥有更细粒度的锁;即使您确实希望停止所有更改(也许您正在序列化对象),您也可以获取所有锁来实现相同的目的,而且这样更显式。当你使用synchronized(this)时,并不清楚你为什么要同步,或者可能的副作用是什么。如果你使用synchronized(labelMonitor),或者更好的labelLock.getWriteLock().lock(),很清楚你在做什么,你的临界区的影响仅限于什么。

简短的回答:你必须理解区别,并根据代码做出选择。

长回答:一般来说,我宁愿尽量避免同步(这)以减少争用,但私有锁会增加你必须意识到的复杂性。因此,对正确的任务使用正确的同步。如果您在多线程编程方面没有经验,我宁愿坚持使用实例锁定并仔细阅读这个主题。(也就是说:仅仅使用同步(这)并不能自动使你的类完全线程安全。)这不是一个简单的话题,但一旦你习惯了,是否使用同步(这)的答案就会自然而然地出现。

我将分别讨论每一点。

    <李> < blockquote > 一些邪恶的代码可能会偷你的锁(很流行的这一个,也有一个 “不小心”变体)< / p >

    我更担心不小心。这意味着this的使用是类的公开接口的一部分,并且应该被记录。有时需要其他代码能够使用您的锁。像Collections.synchronizedMap(参见javadoc)这样的东西也是如此 <李> < blockquote >

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

    这是一种过于简单的想法;仅仅摆脱synchronized(this)并不能解决问题。

    <李> < blockquote >

    你(不必要地)暴露了太多信息

    这是第一条的变体。synchronized(this)的使用是你的接口的一部分。

    .

    .

    .

这取决于你想做的任务,但我不会用它。此外,检查您想要完成的线程保存是否不能首先通过同步(此)来完成?还有一些不错的锁定API可能会帮助你:)

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

假设有以下代码:

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




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

方法1修改变量一个,方法2修改变量b,应该避免两个线程同时修改同一个变量。但是,当thread1修改一个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 ++;
}
// ................
}


}

上面的例子使用了更细粒度的锁(两个锁而不是一个锁(lockAlockB分别用于变量一个b),因此可以实现更好的并发性,另一方面,它变得比第一个例子更复杂……

虽然我同意不要盲目地遵守教条规则,但“偷锁”的场景对你来说是不是很古怪?线程确实可以“外部”(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容器如何工作的看法过于简单,但恕我直言,它证明了这一点。

因此,我将基于安全性考虑做出设计选择:我是否能够完全控制访问实例的代码?线程无限期地持有实例锁的后果是什么?

我所见过的大多数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的挖苦,只是我在这两种语言上工作的经验的反映。

java.util.concurrent包极大地降低了线程安全代码的复杂性。我只有轶事证据,但我所见过的synchronized(x)的大多数工作似乎是重新实现一个锁,信号量,或Latch,但使用较低级别的监视器。

考虑到这一点,使用这些机制中的任何一种进行同步都类似于对内部对象进行同步,而不是泄露锁。这是非常有益的,因为您可以绝对确定通过两个或多个线程控制进入监视器的条目。

如果你已经决定:

  • 你需要做的是锁定 当前对象;和李< / >
  • 你想 锁定粒度小于

那么我就不认为synchronizezd是一个禁忌。

有些人故意在方法的整个内容中使用synchronized(this)(而不是将方法标记为synchronized),因为他们认为这样“对读者更清楚”哪个对象实际上正在被同步。只要人们做出明智的选择(例如,理解这样做实际上是在方法中插入额外的字节码,这可能会对潜在的优化产生连锁反应),我不认为这有什么问题。您应该始终记录程序的并发行为,因此我不认为“‘同步’发布行为”的论点如此引人注目。

至于应该使用哪个对象的锁的问题,我认为在当前对象如果这是您正在做的事情的逻辑所期望的,以及您的类通常是如何使用的上同步没有问题。例如,对于集合,逻辑上期望锁定的对象通常是集合本身。

一个使用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...
}

正如你在这里看到的,我们在这个上使用同步来方便地与那里的一些同步方法进行长周期(可能是无限循环的run方法)合作。

当然,在私有字段上使用synchronized可以很容易地重写。但有时,当我们已经有了一些同步方法的设计时(例如,我们从遗留类派生出来的,synchronized(this)可能是唯一的解决方案)。

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

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

  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(...)时,会出现死锁,因为两个线程都不能获得其他对象的锁

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

这里已经说过,同步块可以使用用户定义的变量作为锁对象,当同步函数只使用“this”时。当然,你也可以对函数中需要同步的部分进行操作。

但是每个人都说synchronized函数和block之间没有区别,block覆盖了使用“this”作为锁对象的整个函数。这是不对的,不同的是字节码,将在这两种情况下产生。在同步块使用的情况下,应该分配本地变量,其中包含引用“this”。因此,我们会得到一个更大的函数(如果你只有几个函数,这就无关紧要了)。

你可以在这里找到更详细的解释: http://www.artima.com/insidejvm/ed2/threadsynchP.html < / p >

同步块的使用也不好,原因如下:

synchronized关键字在一个方面非常有限:当退出一个同步块时,所有等待该锁的线程都必须被解除阻塞,但只有其中一个线程可以获得锁;所有其他人都看到锁已被占用,并返回阻塞状态。这不仅仅是浪费了大量的处理周期:为解除线程阻塞而进行的上下文切换通常还涉及从磁盘调出内存,这是非常非常昂贵的。

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

锁既可以用于可见性,也可以用于保护一些可能导致竞争的并发修改数据。

当你只需要将基本类型操作设置为原子类型时,可以使用AtomicInteger之类的选项。

但是假设你有两个相互关联的整数,比如xy坐标,它们彼此相关,应该以原子的方式进行更改。然后使用相同的锁来保护它们。

锁应该只保护彼此相关的状态。不多不少。如果在每个方法中使用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,那么它就不是线程安全的。

这个例子只是为了演示它的实现方式,而不一定是这样。最好的方法是使它< em >的< / em >

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

使用不同锁来保护不相关状态的过程称为锁剥离或锁分裂

我认为在Brian Goetz的《Java并发实践》一书中有一个很好的解释,解释了为什么这些技术都是非常重要的。他明确指出了一点——必须使用相同的锁“EVERYWHERE”来保护对象的状态。同步方法和对象上的同步通常是携手并进的。Vector同步它所有的方法。如果你有一个vector对象的句柄,并且要做“put If absent”,那么仅仅是vector同步它自己的单独方法并不能保护你免受状态损坏。你需要使用synchronised (vectorHandle)进行同步。这将导致每个具有vector句柄的线程都获得SAME锁,并将保护vector的整体状态。这被称为客户端锁定。事实上,我们知道vector确实同步了(this) /同步了它所有的方法,因此对对象vectorHandle的同步将导致vector对象状态的正确同步。仅仅因为使用了线程安全的集合就认为自己是线程安全的,这是愚蠢的。这正是ConcurrentHashMap显式引入putIfAbsent方法的原因——使此类操作具有原子性。

总之

  1. 方法级的同步允许客户端锁定。
  2. 如果你有一个私有锁对象-它使客户端锁定不可能。如果您知道您的类没有“如果缺席就放置”类型的功能,那么这样做是很好的。
  3. 如果你正在设计一个库,那么在这个库上同步或者同步方法通常是更明智的。因为您很少能够决定如何使用您的类。
  4. 如果Vector使用了一个私有锁对象——它将不可能得到正确的“如果不存在就放”。客户端代码永远不会获得私有锁的句柄,从而打破了使用EXACT SAME lock来保护其状态的基本规则。
  5. 正如其他人指出的那样,在这个或同步方法上同步确实存在一个问题——有人可能获得了一个锁,却永远不会释放它。所有其他线程将继续等待锁被释放。
  6. 所以,知道你在做什么,然后采用正确的方法。
  7. 有人认为,拥有一个私有锁对象可以提供更好的粒度——例如,如果两个操作是不相关的——它们可以被不同的锁保护,从而获得更好的吞吐量。但我认为这是设计的味道,而不是代码的味道——如果两个操作完全不相关,为什么它们属于同一个类?为什么一个类俱乐部应该完全不相关的功能?可能是一个实用类?一些util通过同一个实例提供字符串操作和日历日期格式??... 至少对我来说没有任何意义!!
  1. 如果可能的话,让你的数据不可变(final变量)
  2. 如果你不能避免在多个线程之间共享数据的突变,使用高级编程结构[例如粒度Lock API]

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

使用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()
}
}
}

锁定相对于同步的优势

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

  2. 锁实现通过提供同步方法和语句提供了额外的功能

    1. 获取锁的非阻塞尝试(tryLock())
    2. 试图获取可以被中断的锁(lockInterruptibly())
    3. 试图获取可能超时的锁(tryLock(long, TimeUnit))。
    4. 李< / ol > < / >
    5. Lock类还可以提供与隐式监视锁完全不同的行为和语义,例如

      1. 保证订购
      2. 不可重入使用
      3. 死锁检测
      4. 李< / ol > < / >

      看一下关于各种类型的Locks的SE问题:

      同步vs锁定

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

      < <强> < em >锁对象/ em > < / >强支持简化许多并发应用程序的锁定习惯用法。

      < >强执行人< / >强定义了用于启动和管理线程的高级API。concurrent提供的执行器实现提供了适合大型应用程序的线程池管理。

      < >强并发集合< / >强使它更容易管理大量的数据集合,并可以大大减少同步的需要。

      < >强原子变量< / >强具有最小化同步和帮助避免内存一致性错误的特性。

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

      其他编程结构也可以参考java . util . concurrentjava.util.concurrent.atomic包。

视情况而定 如果只有一个或多个共享实体。< / p >

参见完整的工作示例 在这里

简单介绍一下。

< p > 线程和可共享实体 < br > 多个线程可以访问同一个实体,例如多个connectionThreads共享一个messageQueue。由于线程并发运行,可能会有一个线程的数据被另一个线程覆盖的机会,这可能是一个混乱的情况 因此,我们需要某种方法来确保可共享实体一次只能被一个线程访问。(并发)。< / p > < p > 同步块 < br > Synchronized()块是一种确保可共享实体并发访问的方法 首先,一个小类比
假设有两个人P1, P2(线程)在一个盥洗室里有一个脸盆(可共享实体),并且有一扇门(锁) 现在我们希望一次只有一个人使用脸盆 一种方法是在P1锁门时P2等待P1完成他的工作
P1打开门
那么只有p1可以使用脸盆。< / p >

语法。

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

"this"提供了与类相关的内在锁(Java开发人员设计Object类的方式使每个对象都可以作为监视器工作)。 上述方法适用于只有一个共享实体和多个线程(1:N)的情况 enter image description here N个可共享实体- m个线程 < br > 现在想象这样一种情况:卫生间里有两个脸盆,但只有一扇门。如果我们使用前面的方法,一次只有p1可以使用一个脸盆,而p2将在外面等待。这是资源的浪费,因为没有人使用B2(脸盆) 一个更明智的做法是在洗手间内创建一个小一点的房间,每个洗脸盆都有一扇门。这样,P1可以访问B1, P2可以访问B2,反之亦然。< / p >

washbasin1;
washbasin2;


Object lock1=new Object();
Object lock2=new Object();


synchronized(lock1)
{
washbasin1;
}


synchronized(lock2)
{
washbasin2;
}
< p > enter image description here < br > enter image description here < / p >

查看更多关于线程的信息——> 在这里

这实际上只是对其他答案的补充,但如果你对使用私有对象进行锁定的主要反对意见是,它会使你的类与与业务逻辑无关的字段混淆,那么Project Lombok在编译时使用@Synchronized来生成样板:

@Synchronized
public int foo() {
return 0;
}

编译,

private final Object $lock = new Object[0];


public int foo() {
synchronized($lock) {
return 0;
}
}

我只想提到一种可能的解决方案,用于在没有依赖关系的原子代码部分中惟一的私有引用。您可以使用带锁的静态Hashmap和名为atomic()的简单静态方法,该方法使用堆栈信息(完整的类名和行号)自动创建所需的引用。然后,您可以在同步语句中使用此方法,而无需写入新的锁对象。

// 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)作为锁定机制:这将锁定整个类实例,并可能导致死锁。在这种情况下,重构代码以只锁定特定的方法或变量,这样整个类就不会被锁定。Synchronised可以在方法level中使用 下面的代码展示了如何锁定一个方法,而不是使用synchronized(this)。< / p >
   public void foo() {
if(operation = null) {
synchronized(foo) {
if (operation == null) {
// enter your code that this method has to handle...
}
}
}
}

我对2019年的看法,尽管这个问题本可以已经解决了。

如果你知道你在做什么,锁定'this'并不坏,但在幕后锁定'this'是(不幸的是synchronized关键字在方法定义中允许)。

如果你真的想让你的类的用户能够“窃取”你的锁(即阻止其他线程处理它),你实际上想让所有的同步方法在另一个同步方法运行时等待,以此类推。 它应该是有意的,经过深思熟虑的(因此有文档来帮助你的用户理解它)

更详细地说,反过来,你必须知道如果你锁定了一个不可访问的锁(没有人可以“偷”你的锁,你完全控制等等),你会“获得”(或“失去”)什么。

对我来说,问题是synchronized关键字在方法定义签名使得它太容易为程序员不要去想锁定什么是一个非常重要的事情,如果你不想在多线程程序中遇到问题。

人们不能争辩说,“通常”你不希望你的类的用户能够做这些事情,或者“通常”你想要……这取决于你编写的是什么功能。因为你不能预测所有的用例,所以你不能制定一个经验法则。

例如,prinwriter使用了一个内部锁,但是如果人们不想让他们的输出相互交错,他们就很难从多个线程中使用它。

锁是否可以在类外部访问是程序员根据类的功能来决定的。它是api的一部分。例如,你不能从synchronized(this)移到synchronized(provateObjet)而不冒破坏使用它的代码更改的风险。

注1:我知道你可以实现任何同步(这个)'实现'通过使用显式锁对象和暴露它,但我认为这是不必要的,如果你的行为是很好的记录,你实际上知道什么锁定'this'的意思。

注2:我不同意这样的观点:如果一些代码不小心偷了你的锁,那就是一个bug,你必须解决它。这在某种程度上等同于说我可以让我所有的方法公开,即使它们本不应该是公开的。如果有人“意外”调用我的意图是私人方法,这是一个bug。为什么会发生这样的事故!!如果能偷你的锁对你的类来说是一个问题,那就不要允许它。就这么简单。

让我先把结论说出来——对私有字段的锁定对于稍微复杂一点的多线程程序是不起作用的。这是因为多线程是一个全局问题。本地化同步是不可能的,除非你以一种非常防御的方式写(例如,复制所有传递给其他线程的内容)。


下面是详细的解释:

同步包括三个部分:原子性、可见性和有序性

同步块是非常粗糙的同步级别。正如您所期望的那样,它加强了可见性和排序。但是对于原子性,它并不能提供太多的保护。原子性要求程序的全局知识,而不是局部知识。(这使得多线程编程非常困难)

假设我们有一个类Account,它有方法depositwithdraw。它们都是基于一个私有锁进行同步的,就像这样:

class Account {
private Object lock = new Object();


void withdraw(int amount) {
synchronized(lock) {
// ...
}
}


void deposit(int amount) {
synchronized(lock) {
// ...
}
}
}

考虑到我们需要实现一个更高级别的类来处理传输,就像这样:

class AccountManager {
void transfer(Account fromAcc, Account toAcc, int amount) {
if (fromAcc.getBalance() > amount) {
fromAcc.setBalance(fromAcc.getBalance() - amount);
toAcc.setBalance(toAcc.getBalance + amount);
}
}
}

假设我们现在有两个账户,

Account john;
Account marry;

如果Account.deposit()Account.withdraw()仅使用内部锁锁定。这将导致问题时,我们有2个线程工作:

// Some thread
void threadA() {
john.withdraw(500);
}


// Another thread
void threadB() {
accountManager.transfer(john, marry, 100);
}

因为threadAthreadB可能同时运行。线程B完成条件检查,线程A退出,线程B再次退出。这意味着即使约翰的账户上没有足够的钱,我们也可以从他那里提取100美元。这将打破原子性。

你可能会提出:为什么不将withdraw()deposit()添加到AccountManager中呢?但是根据这个提议,我们需要创建一个多线程安全的Map,它将不同的帐户映射到它们的锁。我们需要在执行后删除锁(否则会泄漏内存)。我们还需要确保没有其他对象直接访问Account.withdraw()。这将引入许多微妙的错误。

正确和最习惯的方法是在Account中公开锁。并让AccountManager使用锁。但在这种情况下,为什么不直接使用对象本身呢?

class Account {
synchronized void withdraw(int amount) {
// ...
}


synchronized void deposit(int amount) {
// ...
}
}


class AccountManager {
void transfer(Account fromAcc, Account toAcc, int amount) {
// Ensure locking order to prevent deadlock
Account firstLock = fromAcc.hashCode() < toAcc.hashCode() ? fromAcc : toAcc;
Account secondLock = fromAcc.hashCode() < toAcc.hashCode() ? toAcc : fromAcc;


synchronized(firstLock) {
synchronized(secondLock) {
if (fromAcc.getBalance() > amount) {
fromAcc.setBalance(fromAcc.getBalance() - amount);
toAcc.setBalance(toAcc.getBalance + amount);
}
}
}
}
}

简而言之,私有锁不适用于稍微复杂一点的多线程程序。

(转自https://stackoverflow.com/a/67877650/474197)