ChatGPT解决这个技术问题 Extra ChatGPT

在 Java 中避免同步(this)?

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

一些给定的原因是:

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

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

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

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

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

因此:您是否应该始终避免使用 synchronized(this) 并将其替换为对私有引用的锁定?

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

我们正在谈论实例同步

隐式(同步方法)和显式同步(this)都被考虑

如果您在该主题上引用 Bloch 或其他权威,请不要遗漏您不喜欢的部分(例如 Effective Java,关于线程安全的项目:通常它是实例本身的锁,但也有例外。)

如果您需要除 synchronized(this) 之外的锁定粒度,则 synchronized(this) 不适用,所以这不是问题

我还想指出,上下文很重要——“通常它是实例本身的锁”位在有关记录条件线程安全类的部分中,当您将锁公开时。换句话说,当你已经做出这个决定时,这句话就适用。
在没有内部同步的情况下,当需要外部同步时,锁通常是实例本身,Bloch 基本上说。那么,为什么在“this”上锁定的内部同步也不是这种情况呢? (文档的重要性是另一个问题。)
在扩展粒度和额外的 CPU 缓存和总线请求开销之间存在权衡,因为锁定外部对象很可能需要在 CPU 缓存之间修改和交换单独的缓存行(参见 MESIF 和 MOESI)。
我认为,在防御性编程的世界中,您不是通过成语而是通过代码来防止错误。当有人问我一个问题“你的同步优化到什么程度?”时,我想说“非常”而不是“非常,除非其他人不遵循成语”。

R
Ravindra babu

我将分别介绍每一点。

一些邪恶的代码可能会偷走你的锁(这个很流行,也有一个“意外”变体)我更担心意外。它的意思是,对 this 的使用是你的类的公开接口的一部分,应该记录在案。有时需要其他代码使用您的锁的能力。 Collections.synchronizedMap 之类的东西也是如此(请参阅 javadoc)。同一个类中的所有同步方法都使用完全相同的锁,这会降低吞吐量这是过于简单的想法;只是摆脱 synchronized(this) 并不能解决问题。吞吐量的适当同步需要更多考虑。您(不必要地)暴露了太多信息这是#1的变体。使用 synchronized(this) 是您界面的一部分。如果你不想/不需要这个暴露,不要这样做。


1.“同步”不是你的类公开接口的一部分。 2. 同意 3. 见 1。
本质上是 synchronized(this) 是公开的,因为这意味着外部代码会影响您的类的操作。所以我断言你必须将它记录为接口,即使语言没有。
相似的。请参阅 Collections.synchronizedMap() 的 Javadoc——返回的对象在内部使用 synchronized(this),他们希望消费者利用它来为像迭代这样的大规模原子操作使用相同的锁。
事实上 Collections.synchronizedMap() 内部没有使用 synchronized(this),它使用了一个私有的最终锁对象。
@Bas Leijdekkers:the documentation 明确指定同步发生在返回的地图实例上。有趣的是,keySet()values() 返回的视图不会锁定(它们的)this,而是锁定地图实例,这对于获得所有地图操作的一致行为很重要。锁定对象被分解为变量的原因是,子类 SynchronizedSortedMap 需要它来实现锁定在原始地图实例上的子地图。
M
Mnementh

嗯,首先应该指出的是:

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

在语义上等价于:

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

这是不使用 synchronized(this) 的原因之一。您可能会争辩说您可以围绕 synchronized(this) 块做一些事情。通常的原因是尽量避免必须进行同步检查,这会导致各种并发问题,特别是 double checked-locking problem,它只是说明了进行相对简单的线程安全检查是多么困难。

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

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

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


“同步(这个)真的没有给你任何东西。”好的,我用同步(myPrivateFinalLock)替换它。这给了我什么?你说它是一种防御机制。我受到什么保护?
您可以防止外部对象意外(或恶意)锁定“this”。
我完全不同意这个答案:锁应该始终保持尽可能短的时间,这正是您想要围绕同步块“做事”而不是同步整个方法的原因.
在同步块之外做事总是善意的。关键是人们很多时候都犯了这个错误,甚至没有意识到,就像在双重检查锁定问题中一样。通往地狱的道路是用善意铺成的。
我总体上不同意“X 是一种防御机制,这绝不是一个坏主意”。由于这种态度,有很多不必要的臃肿代码。
i
informatik01

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

假设以下代码:

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


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

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

}

上面的例子使用了更细粒度的锁(2 个锁而不是一个(lockA 和 lockB 分别用于变量 a 和 b),因此允许更好的并发性,另一方面它变得比第一个例子更复杂......


这是非常危险的。您现在已经介绍了客户端(此类用户的)锁定排序要求。如果两个线程以不同的顺序调用method1()和method2(),它们很可能会死锁,但是这个类的用户并不知道会出现这种情况。
“同步(this)”未提供的粒度超出了我的问题范围。而且您的锁定字段不应该是最终的吗?
为了产生死锁,我们应该执行从 A 同步的块到 B 同步的块的调用。daveb,你错了......
据我所知,此示例中没有死锁。我接受它只是伪代码,但我会使用 java.util.concurrent.locks.Lock 的实现之一,如 java.util.concurrent.locks.ReentrantLock
O
Olivier

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

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

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

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

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

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

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

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

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

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

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

如果 getAttribute 在私有锁上同步,则无法进行此攻击,因为第 3 方代码无法获取此锁。

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

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


它取决于类做什么:如果它是一个“重要”对象,那么锁定私有引用?其他实例锁定就足够了吗?
是的,锁窃取场景对我来说似乎很牵强。每个人都提到它,但谁真正做过或经历过它?如果你“不小心”锁定了一个你不应该锁定的对象,那么这种情况就有一个名称:这是一个错误。修理它。
此外,锁定内部引用并非免于“外部同步攻击”:如果您知道代码的某个同步部分等待外部事件发生(例如文件写入、数据库中的值、计时器事件),您可能可以安排它也阻止。
让我承认我是那些白痴之一,尽管我在年轻时就这样做了。我认为通过不创建显式锁定对象,代码更简洁,而是使用另一个需要参与监视器的私有最终对象。我不知道对象本身对自己进行了同步。你可以想象随之而来的hijinx...
R
Rohit Singh

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

查看完整的工作示例 here

一个小小的介绍。

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

同步块 synchronized() 块是一种保证可共享实体并发访问的方法。首先,一个小类比假设有两个人P1,P2(线程)一个洗脸盆(共享实体)在洗手间内,并且有一个门(锁)。现在我们希望一个人一次使用洗脸盆。一种方法是在门被锁时由 P1 锁门 P2 等到 p1 完成他的工作 P1 解锁门然后只有 p1 可以使用洗脸盆。

句法。

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

https://i.stack.imgur.com/jrbvI.png

washbasin1;  
washbasin2;

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

  synchronized(lock1)
  {
    washbasin1;
  }

  synchronized(lock2)
  {
    washbasin2;
  }

https://i.stack.imgur.com/jqavZ.png

查看更多关于线程----> here


s
serg10

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


或许因为 C# 是一门年轻的语言,他们从 Java 阵营中发现的不良模式中学到了更好的代码?单身人士也少吗? :)
呵呵。很可能是真的,但我不会上钩!有人认为我可以肯定地说 C# 代码中有更多的大写字母;)
只是不正确(说得好听)
j
jamesh

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

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


c
csoeger

如果可能的话,让你的数据不可变(最终变量)

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

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

Lock over Synchronized 的优点(this)

同步方法或语句的使用强制所有锁的获取和释放以块结构的方式发生。锁实现通过提供非阻塞尝试获取锁 (tryLock()) 尝试获取可被中断的锁 (lockInterruptibly()) 尝试获取锁,从而提供了超过使用同步方法和语句的附加功能可以超时(tryLock(long,TimeUnit))。 Lock 类还可以提供与隐式监控锁完全不同的行为和语义,例如保证排序、不可重入使用、死锁检测

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

Synchronization vs Lock

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

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

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

Concurrent Collections 可以更轻松地管理大量数据集合,并且可以大大减少同步需求。

Atomic Variables 具有最大限度减少同步并有助于避免内存一致性错误的功能。

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

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


N
Neil Coffey

如果您已经决定:

您需要做的是锁定当前对象;和

您想以小于整个方法的粒度锁定它;

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

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

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


“如果这是逻辑所预期的……”也是我试图表达的观点。我不明白总是使用私有锁的意义,尽管普遍的共识似乎是它更好,因为它不会伤害并且更具防御性。
S
Sujith Thankachan

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

总之

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


A
Andrzej Doyle

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

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

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


t
tcurdt

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

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


当您说这取决于您的经验时,我是否正确理解您?
首先,它取决于您要编写的代码。只是说当你转向不使用同步(这个)时,你可能需要更多的经验。
N
Narendra Pathai

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

当您只需要使原始类型操作成为原子操作时,可以使用 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 是相关的,如果我给出了两种不同的方法来改变 x 和 {2 } 分开,那么它就不是线程安全的。

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

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

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


1
18446744073709551615

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

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

什么样的访问受此保护?一把锁真的够用吗,不是有人介绍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(...);
        ...
    }
}

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

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


在此示例中,获得死锁的唯一方法是当 BadObject A 在 B 上调用 longOperation,传递 A 的 myListener,反之亦然。并非不可能,但相当复杂,支持我之前的观点。
R
Roman

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

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

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

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

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

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


M
Michael

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

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

编译为

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

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

B
Bart Prokop

使用 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) 可能是唯一的解决方案)。


任何物体都可以用作这里的锁。它不需要是 this。它可能是一个私人领域。
正确,但这个例子的目的是展示如何正确同步,如果我们决定使用方法同步。
R
Ravindra babu

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


K
Kolarčík Václav

我只想提一个可能的解决方案,用于在没有依赖关系的代码的原子部分中实现唯一私有引用。您可以使用带锁的静态 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
}

s
surendrapanday

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

A
Amit Mittal

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

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

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

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

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

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

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

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

注 1:我知道您可以通过使用显式锁定对象并公开它来实现同步(this)“实现”,但我认为如果您的行为有据可查并且您实际上知道锁定“this”是什么意思,那么这是不必要的。

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


H
HKTonyLee

让我先得出结论 - 锁定私有字段不适用于稍微复杂的多线程程序。这是因为多线程是一个全球性问题。除非您以非常防御的方式编写(例如,在传递给其他线程时复制所有内容),否则本地化同步是不可能的。

这是长篇的解释:

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

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

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

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

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

您可能会建议:为什么不在 AccountManager 中添加 withdraw()deposit() 呢?但是在这个提议下,我们需要创建一个多线程安全的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


Y
Yoni Roit

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

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