同步方法中的同步块

Synchronized Block within Synchronized Method

我正在查看包含同步方法的第三方库中的一些代码,在该方法中有一个锁定实例变量的同步块。类似于:

public class Foo {
   final Bar bar = new Bar();

   public synchronized void doSomething() {
       // do something
       synchronized(bar) {
           // update bar
       }
   }
   ...
}

这有意义吗?如果是这样,在同步方法中使用同步语句有什么好处?

鉴于同步方法锁定整个对象,这对我来说似乎是多余的。在处理非私有实例变量时,这种方法是否有意义?

在您的示例中,方法是 锁定 Foo 的实例和对象 bar。其他方法可能只锁定对象 bar.

Foo 的实例

所以,是的,这是有道理的,具体取决于他们在做什么。据推测 bar 保护了一些较小的数据子集,一些方法只需要锁定 bar 即可以线程安全的方式执行它们的操作。

synchronized 的作用

synchronized(在方法上或语句中)创建互斥区 (critical section or, specifically for Java, a reentrant mutex)。线程进入临界区的“关键”是synchronized语句中使用的对象引用。在所有使用相同密钥的块中,一次只能有一个线程(递归地)“拥有”这个“密钥”;也就是说,一次只有一个线程可以进入给定对象引用上的任何块 synchronized

这样的关键部分只是防止您在块内执行的 操作 (变量 read/write)与所有其他锁定的关键部分中的任何其他操作同时发生在同一个对象引用上。 (它不会自动保护对象内的所有变量)。

在Java中,这样的关键部分还创建了一个happens-before契约。

举例

作为一个有点做作的例子:

public class Foo {
    final Bar bar = new Bar();
    
    private int instanceCounter = 0;
    private int barCounter = 0;

    public synchronized void incrementBarCounterIfAllowed() {
        synchronized (bar) {
            if (instanceCounter < 10) barCounter++;
        }
    }

    public synchronized void incrementClassCounter() {
        instanceCounter++;
    }

    public void incrementBarCounter() {
        synchronized (bar) {
            barCounter++;
        }
    }

}

实例变量是否私有对于这种方法是否适用并不重要。在一个 class 中,您可以拥有多个锁对象,每个对象都保护自己的数据集。

然而,这样做的风险是您必须非常严格遵守编码约定,以通过在不同位置以不同顺序锁定两个锁来防止死锁。例如,对于上面的代码,如果您随后从代码中的其他地方执行此操作:

synchronized(myFoo.bar) {
  myFoo.incrementClassCounter();
}

您使用 incrementBarCounterIfAllowed() 方法有死锁的风险

请注意,barCounter 可能是 Bar 等的实例变量 - 为了简洁起见,我避免了这一点在代码示例中。

synchronized 方法的情况下,该引用是对 class 实例(或对 Class<?> 的引用static 方法的 class)。

考虑一下:

public void someMethod() {

    synchronized(bar) {
       // fully accessible before entering the other synchronized bar block
       // but not afterwards 
    }

}

说清楚,如果 2 个块在 相同的 对象上同步,则只同步块。

当你说 "synchronized method locks on the entire object" 时,那不是真的。使用 synchronized only 意味着线程必须先获取该锁,然后才能进入使用该锁的标记为同步的方法或块。用作同步实例方法锁的默认对象是 this。如果将 synchronized 放在静态方法上,则默认值为 this.getClass(),或者您可以指定要用作锁的对象。使用 synchronized 除了使实例字段不可访问之外,不会做任何其他事情。

您可以编写一个 class,其中一些方法或块受一个锁保护,一些方法或块受另一个锁保护,而对于其他方法或块,您需要两个锁。确保以相同的顺序获取锁,否则可能会导致死锁。

我将举一个真实的例子来解释安迪通过代码解释的内容(对于那些觉得难以理解的人):

假设您有一套 1 BHK 公寓。

  1. 进入房间的方式是通过大厅(必须先进入大厅才能进入房间)
  2. 您想限制某人使用您的房间,但他可以use/enter在大厅。

在这种情况下,如果有人从后门进入房间并从里面上锁。只能进大厅,不能进房间,必须等房间里的人开锁。

希望这能让那些觉得难以理解的人明白。