394

每当 SO 上出现关于 Java 同步的问题时,有些人非常急切地指出synchronized(this)应该避免这种情况。相反,他们声称,锁定私人参考是首选。

一些给定的原因是:

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

我有兴趣看到一些现实世界的例子(没有 foobar 的东西),在这些例子中,避免锁定在也能完成这项工作this时更可取。synchronized(this)

因此:您是否应该始终避免synchronized(this)并用锁定私人参考来代替它?


一些进一步的信息(在给出答案时更新):

  • 我们正在谈论实例同步
  • 隐式(synchronized方法)和显式形式synchronized(this)都被考虑
  • 如果您引用 Bloch 或其他有关该主题的权威,请不要遗漏您不喜欢的部分(例如 Effective Java,关于线程安全的项目:通常是实例本身的锁,但也有例外。)
  • 如果您需要除synchronized(this)提供之外的锁定粒度,synchronized(this)则不适用,因此这不是问题
4

23 回答 23

133

我将分别介绍每一点。

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

    我更担心不小心。它的意思是,这种使用this是你的类公开接口的一部分,应该记录在案。有时需要其他代码使用您的锁的能力。这对于诸如Collections.synchronizedMap(参见 javadoc)之类的事情是正确的。

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

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

  3. 您(不必要地)暴露了太多信息

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

于 2009-01-14T11:05:41.177 回答
89

嗯,首先应该指出的是:

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

在语义上等价于:

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

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

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

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

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

于 2009-01-14T10:42:57.277 回答
58

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

假设以下代码:

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


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

方法一修改变量a和方法二修改变量b,应该避免两个线程同时修改同一个变量,确实如此。但是当thread1修改athread2修改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 个锁而不是一个(变量ab分别为lockAlockB),因此允许更好的并发性,另一方面它变得比第一个示例更复杂......

于 2009-01-14T11:23:27.273 回答
16

虽然我同意不要盲目遵守教条规则,但“偷锁”场景对你来说是否如此古怪?一个线程确实可以“从外部”(synchronized(theObject) {...})获取对象上的锁,从而阻塞等待同步实例方法的其他线程。

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

“意外”版本似乎不太可能,但正如他们所说,“做出一些防白痴的东西,有人会发明一个更好的白痴”。

所以我同意这取决于班级做什么学派的思想。


编辑以下 eljenso 的前 3 条评论:

我从来没有遇到过锁盗问题,但这是一个想象的场景:

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

我是您的客户,在您的站点上部署我的“好”servlet。碰巧我的代码包含对getAttribute.

伪装成另一个客户的黑客在您的站点上部署了他的恶意 servlet。它在方法中包含以下代码init

同步 (this.getServletConfig().getServletContext()) {
   而(真){}
}

假设我们共享相同的 servlet 上下文(只要两个 servlet 位于同一虚拟主机上,规范就允许),我的调用getAttribute将永远锁定。黑客在我的 servlet 上实现了 DoS。

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

我承认这个例子是人为的,并且对 servlet 容器如何工作的看法过于简单,但恕我直言,它证明了这一点。

所以我会根据安全考虑做出我的设计选择:我是否可以完全控制可以访问实例的代码?线程无限期地持有一个实例的锁会产生什么后果?

于 2009-01-14T12:27:35.660 回答
13

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

在此处查看完整的工作示例

一个小小的介绍。

线程和可共享实体
多个线程可以访问同一个实体,例如多个连接线程共享一个消息队列。由于线程同时运行,因此可能有机会覆盖另一个数据,这可能是一个混乱的情况。
因此,我们需要某种方法来确保一次只能由一个线程访问可共享实体。(并发)。

同步块
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;
  }

在此处输入图像描述
在此处输入图像描述

在线程上查看更多信息---->在这里

于 2016-09-11T17:26:49.993 回答
12

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

于 2009-01-14T12:31:42.237 回答
7

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

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

于 2009-01-14T14:34:47.823 回答
7
  1. 如果可能,使您的数据不可变(final变量)
  2. 如果您无法避免跨多个线程共享数据的突变,请使用高级编程结构 [例如粒度 LockAPI]

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

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

Lock over Synchronized 的优点(this)

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

  2. Lock 实现提供了比使用同步方法和语句的附加功能

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

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

看看这个关于各种类型的 SE 问题Locks

同步与锁定

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

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

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

并发集合使管理大型数据集合变得更加容易,并且可以大大减少同步的需要。

原子变量具有最小化同步并有助于避免内存一致性错误的功能。

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

有关其他编程结构,请参阅java.util.concurrentjava.util.concurrent.atomic包。

于 2016-04-18T11:05:24.450 回答
5

如果您已经决定:

  • 您需要做的是锁定当前对象;和
  • 您想以小于整个方法的粒度锁定它;

然后我看不到 synchronizezd(this) 的禁忌。

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

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

于 2009-01-14T14:59:48.193 回答
4

我认为在 Brian Goetz 所著的《Java Concurrency In Practice》一书中,有一个很好的解释为什么这些技术都是你掌握的重要技术。他非常清楚地说明了一点——你必须使用相同的锁“EVERYWHERE”来保护你的对象的状态。同步方法和对象上的同步通常是齐头并进的。例如,Vector 同步它的所有方法。如果您有一个向量对象的句柄并且要执行“如果不存在则放置”,那么仅仅 Vector 同步它自己的各个方法并不能保护您免受状态损坏。您需要使用 synchronized (vectorHandle) 进行同步。这将导致每个拥有向量句柄的线程都获取相同的锁,并将保护向量的整体状态。这称为客户端锁定。事实上,我们知道向量确实同步(this)/同步其所有方法,因此在对象 vectorHandle 上同步将导致向量对象状态的正确同步。仅仅因为您使用线程安全集合就相信您是线程安全的,这是愚蠢的。这正是 ConcurrentHashMap 显式引入 putIfAbsent 方法的原因——使此类操作具有原子性。

总之

  1. 在方法级别同步允许客户端锁定。
  2. 如果您有一个私有锁定对象 - 它会使客户端锁定成为不可能。如果您知道您的课程没有“如果不存在则放置”类型的功能,这很好。
  3. 如果您正在设计一个库 - 那么在此同步或同步该方法通常更明智。因为你很少能够决定如何使用你的类。
  4. 如果 Vector 使用了私有锁对象 - 就不可能正确地“放置如果不存在”。客户端代码永远不会获得私有锁的句柄,从而破坏了使用 EXACT SAME LOCK 保护其状态的基本规则。
  5. 正如其他人指出的那样,在此或同步方法上同步确实存在问题 - 有人可能会获得锁而永远不会释放它。所有其他线程将继续等待锁被释放。
  6. 所以知道你在做什么,并采用正确的那个。
  7. 有人争辩说,拥有私有锁对象可以为您提供更好的粒度 - 例如,如果两个操作不相关 - 它们可以由不同的锁保护,从而提高吞吐量。但我认为这是设计气味而不是代码气味 - 如果两个操作完全不相关,为什么它们是 SAME 类的一部分?一个班级俱乐部为什么要完全不相关的功能?可能是实用程序类?嗯 - 一些通过同一实例提供字符串操作和日历日期格式的工具?……至少对我没有任何意义!!
于 2014-11-04T09:11:07.387 回答
3

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

一般来说,同步有点笨拙,因为它会锁定一个大锁,而不是准确地考虑如何允许线程相互工作。使用synchronized(this)更加笨拙和反社会,因为它是在说“当我持有锁时,没有人可以改变这门课上的任何东西”。您实际上需要多久执行一次?

我宁愿拥有更细粒度的锁;即使您确实想阻止一切更改(也许您正在序列化对象),您也可以获取所有锁来实现相同的目的,而且这种方式更加明确。当您使用synchronized(this)时,不清楚您要同步的确切原因,或者可能有什么副作用。如果您使用synchronized(labelMonitor),甚至更好labelLock.getWriteLock().lock(),很清楚您在做什么以及您的关键部分的影响仅限于什么。

于 2009-01-14T10:50:44.640 回答
3

简短回答:您必须了解差异并根据代码做出选择。

长答案:一般来说,我宁愿尽量避免synchronize(this)以减少争用,但私有锁会增加您必须注意的复杂性。因此,为正确的工作使用正确的同步。如果您对多线程编程没有那么丰富的经验,我宁愿坚持实例锁定并阅读该主题。(也就是说:仅使用synchronize(this)并不会自动使您的类完全线程安全。)这不是一个简单的话题,但是一旦您习惯了它,是否使用synchronize(this)的答案就很自然了.

于 2009-01-14T11:04:53.893 回答
2

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

当您只需要使原始类型操作成为原子操作时,可以使用诸如此类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;
   }
}

在上面的示例中,我只有一种方法可以同时改变两种不同的方法xy而不是两种不同的方法,x并且y如果我分别给出了两种不同的变异方法,x那么y它就不是线程安全的。

这个例子只是为了演示而不一定是它应该实现的方式。最好的方法是使它成为IMMUTABLE

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

使用不同锁保护不相关状态的过程称为Lock Striping 或 Lock Splitting

于 2013-11-20T07:43:42.890 回答
1

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

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

  1. 什么样的访问受此保护?
  2. 一把锁真的够用吗,不是有人介绍bug吗?

一个例子:

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(...);
        ...
    }
}

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

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

于 2013-02-20T06:26:52.887 回答
1

正如这里已经说过的,同步块可以使用用户定义的变量作为锁对象,当同步函数只使用“this”时。当然,您可以使用应该同步的功能区域等进行操作。

但是每个人都说同步函数和使用“this”作为锁定对象覆盖整个函数的块之间没有区别。这不是真的,区别在于两种情况下都会生成的字节码。在同步块使用的情况下,应分配局部变量,该变量包含对“this”的引用。结果,我们将拥有更大的函数大小(如果您只有少数函数,则不相关)。

您可以在此处找到有关差异的更详细说明: http ://www.artima.com/insidejvm/ed2/threadsynchP.html

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

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

有关这方面的更多详细信息,我建议您阅读这篇文章:http: //java.dzone.com/articles/synchronized-considered

于 2013-07-16T08:50:32.087 回答
1

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

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

编译为

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

public int foo() {
    synchronized($lock) {
        return 0;
    }
}
于 2018-10-29T08:38:01.853 回答
0

这取决于您要执行的任务,但我不会使用它。另外,首先检查您想要完成的线程保存是否无法通过 synchronize(this) 完成?API中还有一些不错的可能会对您有所帮助:)

于 2009-01-14T11:16:20.093 回答
0

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

正如您在此处看到的,我们在此使用 synchronize 以轻松地与那里的一些同步方法进行长时间(可能是 run 方法的无限循环)的协作。

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

于 2010-07-13T12:38:54.900 回答
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
}
于 2018-11-18T09:17:06.277 回答
0

避免synchronized(this)用作锁定机制:这会锁定整个类实例并可能导致死锁。在这种情况下,重构代码以仅锁定特定的方法或变量,这样整个类就不会被锁定。Synchronised可以在方法级别使用。
下面的代码没有使用synchronized(this),而是显示了如何锁定一个方法。

   public void foo() {
if(operation = null) {
    synchronized(foo) { 
if (operation == null) {
 // enter your code that this method has to handle...
          }
        }
      }
    }
于 2019-02-26T00:20:03.457 回答
0

尽管这个问题本可以解决,但我在 2019 年的两分钱。

如果您知道自己在做什么,那么锁定“this”还不错,但是在幕后锁定“this”是(不幸的是,方法定义中的同步关键字允许这样做)。

如果您确实希望您的类的用户能够“窃取”您的锁(即阻止其他线程处理它),您实际上希望所有同步方法在另一个同步方法运行时等待,等等。它应该是有意的和深思熟虑的(因此记录在案以帮助您的用户理解它)。

更详细地说,相反,如果您锁定不可访问的锁(没有人可以“偷”您的锁,您完全可以控制等等,那么您必须知道您正在“获得”(或“失去”)什么。 ..)。

对我来说,问题是方法定义签名中的 synchronized 关键字让程序员很容易不去考虑要锁定什么,如果你不想在 multi -线程程序。

不能说“通常”您不希望您班级的用户能够做这些事情,或者“通常”您想要......这取决于您正在编码的功能。您无法制定拇指规则,因为您无法预测所有用例。

例如,考虑使用内部锁的 printwriter,但是如果他们不希望他们的输出交错,那么人们很难从多个线程中使用它。

您的锁是否可以在类之外访问是您作为程序员的决定,具体取决于类具有什么功能。它是 api 的一部分。例如,您不能从 synchronized(this) 移到 synchronized(provateObjet) 而不冒破坏使用它的代码更改的风险。

注意 1:我知道您可以通过使用显式锁定对象并公开它来实现任何同步(this)“实现”,但我认为如果您的行为有据可查并且您实际上知道锁定“this”的含义,则没有必要。

注意 2:我不同意如果某些代码不小心窃取了您的锁,这是一个错误,您必须解决它的论点。这在某种程度上与说我可以将所有方法公开,即使它们不打算公开也是一样的论点。如果有人“不小心”称我打算成为私有方法,则它是一个错误。为什么首先要启用这个事故!!!如果窃取你的锁的能力对你的班级来说是一个问题,那就不要允许它。就如此容易。

于 2019-07-17T06:21:31.103 回答
0

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

同步块是非常粗略的同步级别。它按照您的预期强制执行可见性和排序。但是对于原子性,它并没有提供太多的保护。原子性需要程序的全球知识而不是本地知识。(这使得多线程编程非常困难)

假设我们有一个Account具有方法deposit和的类withdraw。它们都是基于这样的私有锁同步的:

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);
        }
    }
}

假设我们现在有 2 个帐户,

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);
}

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

您可能会建议:为什么不添加withdraw()anddeposit()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

于 2021-06-07T19:40:06.143 回答
-3

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

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

于 2009-01-14T10:43:12.517 回答