避免在Java中同步(this)?


381

每当关于Java同步的问题浮出水面时,有些人就会很想指出synchronized(this)应该避免的事情。他们声称,取而代之的是,最好是锁定私有引用。

给出的一些原因是:

包括我在内的其他人则认为,这synchronized(this)是一个惯用语言(在Java库中也是如此),是安全且易于理解的。应当避免这种情况,因为您有一个错误,而且对多线程程序中正在发生的事情一无所知。换句话说:如果适用,请使用它。

我对看到一些真实的示例(没有foobar的东西)感兴趣,在这种情况下,避免锁定this也更可取synchronized(this)

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


一些进一步的信息(根据给出的答案进行更新):

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

4
我还要指出,上下文很重要-“通常是实例本身的锁”位在有关公开条件锁的情况下,该文档用于记录有条件的线程安全类。换句话说,当您已经做出此决定时,该句子才适用。
乔恩·斯基特

Bloch基本上说,在没有内部同步的情况下,并且当需要外部同步时,锁通常是实例本身。那么为什么不对“ this”进行锁定的内部同步也是如此呢?(文档的重要性是另一个问题。)
eljenso,2009年

在扩展的粒度与额外的CPU缓存和总线请求开销之间需要权衡,因为锁定外部对象很可能需要修改单独的缓存行并在CPU缓存之间进行交换(请参阅MESIF和MOESI)。
ArtemGr 2010年

1
我认为,在防御性编程领域,您不是通过惯用语言而是通过代码来防止错误。当有人问我“如何优化同步?”时,我想说的是“非常”,而不是“非常,除非其他人不遵循这个惯用法”。
Sgene9

Answers:


132

我将分别讨论每个要点。

  1. 一些邪恶的代码可能会窃取您的锁(此锁非常流行,还具有“意外”变体)

    我更担心意外。这意味着该用法this是您的类的公开接口的一部分,应进行记录。有时需要其他代码使用您的锁的能力。诸如此类的事情都是如此Collections.synchronizedMap(请参阅javadoc)。

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

    这太过简单了。仅仅摆脱synchronized(this)将无法解决问题。正确同步吞吐量需要更多的考虑。

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

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


1.“同步”不是您的类的公开接口的一部分。2.同意3.见1.
eljenso,2009年

66
从本质上讲,同步(this)公开的,因为这意味着外部代码可以影响您的类的操作。因此,我断言即使该语言没有,您也必须将其记录为接口。
达伦(Darron)在2009年

15
类似。请参阅Javadoc for Collections.synchronizedMap()-返回的对象在内部使用了sync(this),并且他们希望使用者可以利用它对大规模原子操作(例如迭代)使用相同的锁。
达伦(Darron)

3
实际上,Collections.synchronizedMap()在内部未使用synced(this),而是使用了私有的最终锁定对象。
Bas Leijdekkers '16

1
@Bas Leijdekkers:文档明确指定了同步在返回的地图实例上进行。有趣的是,返回的视图keySet()values()不会锁定(它们的)视图this,而是映射实例,这对于获得所有映射操作的一致行为很重要。锁定对象被分解为变量的原因是,子类SynchronizedSortedMap需要它来实现锁定原始地图实例的子地图。
Holger

86

好吧,首先应该指出的是:

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

在语义上等效于:

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

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

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

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

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


4
“同步的(这个)实际上并没有给您任何东西。” 好的,我将其替换为sync(myPrivateFinalLock)。那给我什么?您谈论它是一种防御机制。我有什么保护?
eljenso

14
您可以防止外部对象意外(或恶意)锁定“ this”。
cletus

14
我完全不同意这个答案:应该始终将锁定保持最短的时间,这正是您想要围绕同步块“执行操作”而不是同步整个方法的原因。
奥利维尔,2009年

5
在同步块之外执行操作始终是好主意。关键是人们经常错误地发现它,甚至没有意识到,就像在双重检查的锁定问题中一样。通向善意的道路铺平了道路。
cletus

16
我通常不同意“ X是一种防御机制,这绝不是一个坏主意”。由于这种态度,那里有很多不必要的code肿代码。
finnw

54

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

假设以下代码:

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


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

方法1修改变量a和方法2修改变量b,应该避免两个线程同时修改同一变量。但是,在线程1修改a线程2修改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)),因此允许更好的并发性,另一方面,它比第一个示例更复杂...


这是非常危险的。现在,您已经介绍了客户端(此类的用户)锁排序要求。如果两个线程以不同的顺序调用method1()和method2(),则它们很可能会死锁,但是此类的用户不知道是这种情况。
daveb

7
“同步(this)”未提供的粒度超出了我的问题范围。而且您的锁定字段不应该是最终的吗?
eljenso

10
为了产生死锁,我们应该执行从A同步的块到B. daveb同步的块的调用,您错了……
Andreas Bakurov 09年

3
据我所知,此示例中没有死锁。我接受这只是伪代码,但我会使用java.util.concurrent.locks.Lock的一种实现,如java.util.concurrent.locks.ReentrantLock
Shawn Vader

15

尽管我同意不盲目遵循教条主义规则,但“锁窃”方案对您来说似乎如此古怪吗?线程确实可以“外部地”(synchronized(theObject) {...})获取对象的锁,从而阻止其他线程等待同步的实例方法。

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

“偶然的”版本似乎不太可能,但正如他们所说,“制造出防白痴的东西,有人会发明更好的白痴”。

所以我同意这取决于班级的思想流派。


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

我从未遇到过盗锁问题,但这是一个假想的情况:

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

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

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

同步(this.getServletConfig()。getServletContext()){
   while(true){}
}

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

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

我承认该示例是人为设计的,并且对servlet容器的工作方式过于简单化,但是恕我直言,这证明了这一点。

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


它取决于类的作用:如果它是“重要”对象,则锁定私有引用?其他实例锁定就足够了吗?
eljenso

6
是的,锁盗窃案对我而言似乎遥不可及。每个人都提到它,但实际上是谁做过或经历过的呢?如果您“意外”锁定了不应该锁定的对象,那么这种情况就有一个名称:这是一个错误。修理它。
eljenso

4
同样,锁定内部引用也不能免于“外部同步攻击”:如果您知道代码的某个同步部分等待外部事件发生(例如,文件写入,DB中的值,计时器事件),则可能安排它也阻止。
eljenso

让我承认我是那些白痴中的一员,尽管我年轻时就做到了。我认为代码没有创建显式的锁定对象,而是使用需要参与监视器的另一个私有最终对象,从而使代码更简洁。我不知道对象本身对自己进行了同步。您可以想象随之而来的hijinx ...
Alan Cabrera

12

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

在这里查看完整的工作示例

一个小介绍。

线程和可共享实体
多个线程可以访问同一实体,例如共享单个messageQueue的多个connectionThreads。由于线程是同时运行的,因此有可能被一个人的数据覆盖,这可能是一个混乱的情况。
因此,我们需要某种方法来确保可共享实体一次只能被一个线程访问。(准确性)。

同步块
synceded()块是确保可共享实体并发访问的一种方式。
首先,进行一个小类比
假设在洗手间内有两个人的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;
  }

在此处输入图片说明
在此处输入图片说明

查看更多关于主题 ----> 点击这里


11

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


3
也许因为C#是一种较年轻的语言,所以他们从Java阵营中发现的错误模式中学习到了类似的代码,从而更好地学习了?还有更少的单身人士吗?:)
Bill K

3
呵呵。可能是对的,但我不会惹恼了!我想可以肯定地说的是,C#代码中有更多的大写字母;)
serg10

1
只是不正​​确(说得很好)
tcurdt

7

java.util.concurrent软件包大大降低了我的线程安全代码的复杂性。我只有传闻证据,但我见过的大多数工作synchronized(x)似乎都是重新实现Lock,Semaphore或Latch,但使用较低级别的监视器。

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


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

同步锁定的优点(this)

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

  2. 锁实现通过提供以下方法,提供了更多使用同步方法和语句的功能:

    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包。


5

如果您决定:

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

那么我看不到对syncnizezd(this)的禁忌。

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

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


1
“如果逻辑上可以预料到这……”,我也想指出这一点。我不认为总是使用私有锁是有意义的,尽管通常的共识似乎是更好,因为它不会造成伤害并且更具防御性。
eljenso

4

我认为,在Brian Goetz撰写的名为Java Concurrency In Practice的书中,为什么每种方法都是您掌握的至关重要的技术都有很好的解释。他非常明确地指出了这一点-您必须使用相同的锁“ EVERYWHERE”来保护对象的状态。同步方法和对象同步通常是并行的。例如,Vector会同步其所有方法。如果您拥有矢量对象的句柄,并且打算执行“如果不存在,则将其放入”,那么仅Vector同步其自己的各个方法并不能保护您免受状态破坏的影响。您需要使用同步(vectorHandle)进行同步。这将导致每个具有向量指针的线程都将获取SAME锁定,并将保护向量的整体状态。这称为客户端锁定。实际上,我们确实知道vector确实同步了(this)/同步了其所有方法,因此在对象vectorHandle上进行同步将导致矢量对象状态的正确同步。仅仅因为您正在使用线程安全集合而认为您是线程安全的,这是愚蠢的。这正是ConcurrentHashMap明确引入putIfAbsent方法的原因-使此类操作成为原子操作。

综上所述

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

3

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

通常,同步有点笨拙,因为它会造成很大的锁定,而不是确切地考虑如何允许线程相互工作。使用synchronized(this)甚至更加笨拙和反社会,因为它说“没有人可以改变任何东西当我握住锁时,这个班上的 ”。您实际上需要多久进行一次操作?

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


3

简短答案:您必须了解差异并根据代码进行选择。

长答案:通常,我宁愿避免使用sync(this)减少争用,但是私有锁会增加您必须了解的复杂性。因此,对正确的作业使用正确的同步。如果您不熟悉多线程编程,我宁愿坚持实例锁定并继续阅读本主题。(这就是说:仅使用syncnize(this)不会自动使您的类完全具有线程安全性。)这不是一个容易的话题,但是一旦您习惯了它,是否使用syncnize(this)的答案就自然而然地出现了。 。


当您说这取决于您的经验时,我是否能正确理解您?
eljenso

首先,它取决于您要编写的代码。只是说您在转移到不使用sync(this)时可能需要更多的经验。
tcurdt

2

锁用于可见性或用于防止某些数据被并发修改而导致竞争。

当您只需要使基本类型操作成为原子操作时,可以使用诸如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,并y分别那就不会是线程安全的。

这个例子只是为了说明,而不一定是应该实现的方式。最好的方法是将其设置为IMMUTABLE

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

使用不同的锁来保护无关状态的过程称为“ 锁条”或“锁分裂”


1

该理由不上同步是,有时你需要多个锁(第二锁往往经过一些额外的考虑被删除,但你仍然需要在中间状态)。如果你锁定这个,你总是要记住这两个锁之一就是这个 ; 如果您锁定私有对象,则变量名将告诉您。

从读者的角度来看,如果您看到对此有所锁定,则必须始终回答以下两个问题:

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

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

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


2
在此示例中,陷入僵局的唯一方法是BadObjectA调用longOperationB并传递A myListener,反之亦然。并非没有,但很令人费解,支持了我之前的观点。
eljenso

1

如前所述,当同步功能仅使用“ this”时,同步块可以将用户定义的变量用作锁定对象。当然,您可以对应该同步的功能区域进行操作等等。

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

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

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

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

有关此区域的更多详细信息,建议您阅读本文:http : //java.dzone.com/articles/synchronized-considered


1

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

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

编译为

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

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

0

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

如您在这里看到的,我们在此使用同步来轻松地与其中的某些同步方法长时间(可能是运行方法的无限循环)配合。

当然,使用私有字段上的同步可以很容易地重写它。但是有时候,当我们已经有一些使用同步方法的设计时(例如,我们从中继承的旧类,synchronized(this)可能是唯一的解决方案)。


任何对象都可以用作此处的锁。不必如此this。它可能是一个私人领域。
finnw 2010年

正确,但是此示例的目的是演示如果我们决定使用方法同步,如何进行正确的同步。
巴特·普罗科普


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
}

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

0

即使这个问题本来可以解决的,我在2019年的两分钱也可以解决。

如果知道自己在做什么,则锁定“ this”还不错,但在幕后锁定“ this”是很遗憾的(不幸的是,方法定义中允许使用synced关键字)。

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

为了进一步阐述,相反,如果您锁定一个不可访问的锁(没有人可以“窃取”您的锁,您处于完全控制权,等等),那么您必须知道您正在“获取”(或“丢失”)什么。 ..)。

对我来说,问题是方法定义签名中的synced关键字使程序员很容易不考虑锁定哪个内容,如果您不想在多个操作中遇到问题,则要考虑一个非常重要的事情线程程序。

不能说“通常”是您不希望班级的用户能够做这些事情,还是“通常”是您想要的……这取决于您要编码的功能。您无法制定经验法则,因为您无法预测所有用例。

例如考虑使用内部锁的printwriter,但是如果人们不希望其输出交错,人们就会在多个线程中使用它。

您的锁是否可以在类之外访问,是您根据类的功能决定作为程序员的决定。它是api的一部分。例如,您不能从已同步(this)移至已同步(provateObjet),而不必冒险破坏使用该代码的代码更改。

注1:我知道您可以通过使用显式锁定对象并将其公开来实现任何已同步的“此目标”,但是我认为,如果您的行为得到了充分的记录,并且您实际上知道锁定“此”的含义,则没有必要。

注2:我不同意这样的说法:如果某些代码意外地窃取了您的锁,则它是一个bug,您必须解决它。从某种意义上说,这与说我可以将我的所有方法公开(即使它们并不意味着要公开)一样。如果有人“偶然地”称呼我打算作为私有方法,那是一个错误。为什么要首先启用此事故!!!如果对您的班级来说,盗窃锁的能力是一个问题,那就不要这样做。就如此容易。


-3

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

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.