如何在不同的类之间使用同步块?

15

我希望了解如何在不同的类之间使用同步块。我的意思是,我想在多个类中使用同步块,但它们都会同步于同一对象。我能想到的唯一方法是这样:

//class 1
public static Object obj = new Object();

someMethod(){
     synchronized(obj){
         //code
     }
}


//class 2
someMethod(){
     synchronized(firstClass.obj){
         //code
     }
}

在这个例子中,我在第一个类中创建了一个任意的对象来进行同步,在第二个类中也通过静态引用对其进行了同步。然而,这似乎是糟糕的编码方式。是否有更好的方法来实现这一点?


2
让不同的类在同一个对象上进行同步听起来像是一个设计缺陷。重新思考你的设计吧! - Nir Alfasi
@alfasin,你为什么认为这是一个设计缺陷? - Miserable Variable
@alfasin所以我一开始就问是否有其他方法来做这个...因为对我来说,它看起来像是糟糕的代码。我不希望一个类依赖于另一个类,但我仍然希望在相同对象上进行同步,以防止不同线程同时调用这些代码块。 - user3843164
1
耦合并不是本质上的坏事。我看不出为什么密切相关的类不能使用一个共同的锁对象。 - Miserable Variable
2
@MiserableVariable 同步通常是为了保持并发访问的(共享)状态的一致性而需要的。通过确保不变量始终为真,即使在多线程环境中也可以获得状态一致性。如果不同的类需要相同的锁对象,则意味着不变量分布在这些类之间。即使在单线程系统中,这通常都是不良设计。 - isnot2bad
显示剩余7条评论
7个回答

11

通常情况下,使用静态对象作为锁是不太理想的,因为整个应用程序中只有一个线程能够执行。如果多个类共享同一个锁,则更糟糕,可能会导致程序几乎没有并发性。

Java 对每个对象都有内置锁,这样对象可以使用同步来保护自己的数据。线程调用对象的方法时,如果该对象需要防止并发更改,则可以在对象的方法上添加 synchronized 关键字,以便每个调用线程必须获取该对象的锁定才能执行其方法。这样,对不相关的对象的调用不需要相同的锁定,您就有更好的机会使代码实际上同时运行。

锁定不一定是并发的首选技术。实际上,有许多技术可供选择。按优先级降序排列:

1)尽量消除可变状态。不可变对象和无状态函数很理想,因为没有状态需要保护,也不需要锁定。

2)尽可能使用线程限制。如果可以将状态限制为单个线程,则可以避免数据竞争和内存可见性问题,并将锁定的数量最小化。

3)使用并发库和框架而非编写带锁定的自定义对象。熟悉 java.util.concurrent 中的类。这些类要比应用程序开发人员编写的任何东西都更好。

当您已经尽力完成了上述 1、2 和 3 的工作后,可以考虑使用锁定(其中锁定包括 ReentrantLock 等选项)。将锁与正在保护的对象关联起来可以将锁的范围最小化,以便线程不会持有锁的时间超过必要时间。

此外,如果锁定不在被锁定的数据上,那么如果某个时刻您决定使用不同的锁,则避免死锁可能会很具有挑战性。在需要保护的数据结构上进行锁定使得锁定行为更容易推理。

建议完全避免内置锁可能是过早的优化。首先确保只在必要时才在正确的对象上进行锁定。


好的观点,我同意这是最后的选择,但在某些情况下需要慢速处理。我的意思是,如果其他类似的任务正在执行,您希望阻止该任务,我在Jenkins管道中遇到了一个类似的情况,需要禁用跨分支的并发构建。 - Gaurava Agarwal

6

选项1:

更简单的方法是使用枚举或静态内部类创建一个单独的对象(单例模式),然后将其用于锁定两个类,看起来很优雅:

// use any singleton object, at it's simplest can use any unique string in double quotes
  public enum LockObj {
    INSTANCE;
  }

  public class Class1 {
    public void someMethod() {
      synchronized (LockObj.INSTANCE) {
        // some code
      }
    }
  }

  public class Class2 {
    public void someMethod() {
      synchronized (LockObj.INSTANCE) {
        // some code
      }
    }
  }

选项2

JVM可以确保每个字符串只在JVM中出现一次,因此您可以使用任何字符串。唯一性是为了确保该字符串上没有其他锁定存在。根本不要使用此选项,仅用于澄清概念。

     public class Class1 {
    public void someMethod() {
      synchronized ("MyUniqueString") {
        // some code
      }
    }
  }

   public class Class2 {
        public void someMethod() {
          synchronized ("MyUniqueString") {
            // some code
          }
        }
      }

3
锁定字符串常量对我来说是一个可怕的想法。据我所知,跨类别的字符串常量去重是一种实现细节,不具有可移植性。而且总会存在风险,即其他开发人员恰好使用同一个字符串常量进行同步,这可能会导致死锁或其他不良行为。 - Mike Strobel
1
在这里锁定一个字符串真的很可怕,就像 @Mike 说的那样。我不建议别人这样做。 - Nathan Hughes
在字符串上使用锁定时,有时当人们看到简单形式的对象时,概念会更清晰。 - Gaurava Agarwal

3

我认为你想要做的是这样的。你有两个工作类,在同一个上下文对象上执行一些操作。然后你想在上下文对象上锁定这两个工作类。那么以下代码将适用于你。

public class Worker1 {

    private final Context context;

    public Worker1(Context context) {
        this.context = context;
    }

    public void someMethod(){
        synchronized (this.context){
            // do your work here
        }
    }
}

public class Worker2 {

    private final Context context;

    public Worker2(Context context) {
        this.context = context;
    }

    public void someMethod(){
        synchronized (this.context){
            // do your work here
        }
    }
}


public class Context {

    public static void main(String[] args) {
        Context context = new Context();
        Worker1 worker1 = new Worker1(context);
        Worker2 worker2 = new Worker2(context);

        worker1.someMethod();
        worker2.someMethod();
    }
}

如果我的回答解决了您的问题,请将其标记为已接受的答案。如果没有,欢迎您评论您的问题。 - Dulaj Atapattu

3

你的代码对我来说似乎是有效的,即使看起来不太漂亮。但请确保同步的对象为 final。

不过,在实际情况下可能需要考虑其他方面的因素。

无论如何,请在 Javadocs 中清楚地说明你想要实现什么目标。

另一种方法是使用 FirstClass 进行同步。

synchronized (FirstClass.class) {
// do what you have to do
} 

然而,FirstClass 中的每个 synchronized 方法都与上面的同步块相同。换句话说,它们也在同一个对象上进行同步。- 根据上下文可能更好。
在其他情况下,如果需要在数据库访问或类似情况下进行同步,则可能更喜欢一些 BlockingQueue 实现。

synchronized (FirstClass.class) 将导致没有两个线程同时执行。当使用不同的 firstClass 对象时,firstClass.obj 允许多个线程并发执行。 - Miserable Variable
@MiserableVariable 在问题中,firstClass.obj 是静态的。因此它绑定在一个线程上。 - Zarathustra
1
谷歌是你的朋友 ;) http://docs.oracle.com/javase/7/docs/api/java/util/concurrent/BlockingQueue.html - Zarathustra
为什么它是静态的会有所不同? - user3843164
如果它是静态的,那么这个对象在每个 JVM 中只存在一次,因此所有线程都将在同一个对象上进行同步。如果不是静态的,则类的每个实例都将有其自己的同步对象(每个实例同步)。 - Zarathustra
那么它必须是静态的,因为我希望所有线程都在其上同步。 - user3843164

2

我认为你正在错误地使用同步块。自从Java 1.5以来,有一个名为java.util.concurrent的包,可以在同步问题上提供高级控制。

例如,有一个名为Semaphore的类,它提供了一些基本工作,您只需要简单的同步:

Semaphore s = new Semaphore(1);
s.acquire();
try {
   // critical section
} finally {
   s.release();
}

即使是这个简单的类,也比synchronized提供了更多的功能,例如可以使用tryAcquire()方法,它会立即返回锁是否被获取,并留给您选择在锁变得可用之前执行非关键性工作的选项。
使用这些类还可以更清楚地了解对象的目的。虽然通用监视器对象可能会被误解,但默认情况下,Semaphore与线程相关联。
如果您深入研究并发包,您将找到更具体的同步类,例如ReentrantReadWriteLock,它允许定义可能存在许多并发读操作,而只有写操作实际上与其他读/写同步。您将找到一个Phaser,它允许您同步线程以便按顺序执行特定任务(类似于synchronized的相反),以及许多数据结构,在某些情况下可以使同步不必要。
总的来说:除非您确切知道原因或者被困在Java 1.4中,否则根本不要使用纯synchronized。它很难阅读和理解,而且您最有可能正在实现SemaphoreLock的较高功能的至少一部分。

2
针对您的场景,我建议您编写一个帮助类,通过特定的方法返回监视器对象。方法名称本身定义了锁对象的逻辑名称,有助于提高代码可读性。
public class LockingSupport {
    private static final LockingSupport INSTANCE = new LockingSupport();

    private Object printLock = new Object();
    // you may have different lock
    private Object galaxyLock = new Object();

    public static LockingSupport get() {
        return INSTANCE;
    }

    public Object getPrintLock() {
        return printLock;
    }

    public Object getGalaxyLock() {
        return galaxyLock;
    }
}

在你希望强制同步的方法中,你可以请求支持返回适当的锁对象,如下所示。

public static void unsafeOperation() {
    Object lock = LockingSupport.get().getPrintLock();
    synchronized (lock) {
        // perform your operation
    }
}

public void unsafeOperation2() { //notice static modifier does not matter
    Object lock = LockingSupport.get().getPrintLock();
    synchronized (lock) {
        // perform your operation
    }
}

以下是一些优点:
  • 通过采用这种方法,您可以使用方法引用查找所有使用共享锁的位置。
  • 您可以编写高级逻辑来返回不同的锁对象(例如,基于调用者的类包返回整个包中所有类的相同锁对象,但对于其他包的类,则返回不同的锁对象等)。
  • 您可以逐步升级Lock实现以使用java.util.concurrent.locks.LockAPI,如下所示。

例如(更改锁对象类型不会破坏现有代码,尽管将Lock对象用作同步(lock)不是好主意)

public static void unsafeOperation2() {
    Lock lock = LockingSupport.get().getGalaxyLock();
    lock.lock();
    try {
        // perform your operation
    } finally {
        lock.unlock();
    }
}

希望这有所帮助。

1
首先,你当前的方法存在以下问题:
  1. 锁对象没有被称为lock或类似名称。(是的...有点挑剔)
  2. 变量不是final。如果意外(或故意)更改了obj,你的同步将会出现问题。
  3. 变量是public。这意味着其他代码可能通过获取锁来引起问题。
我想象一些这些影响在你的评论中根源上:"这对我来说似乎是糟糕的编码"。
在我看来,这里有两个基本问题:
  1. 你有一个泄漏的抽象。以任何方式(作为公共或包私有变量或通过getter)在“class 1”之外发布锁对象都会暴露锁定机制。应该避免这种情况。

  2. 使用单个“全局”锁意味着你有一个并发瓶颈。

第一个问题可以通过抽象出锁定来解决。例如:
someMethod() {
     Class1.doWithLock(() -> { /* code */ });
}

其中doWithLock()是一个静态方法,接受一个RunnableCallable等类型的参数,并使用适当的锁运行它。 doWithLock()的实现可以使用自己的private static final Object lock或根据其规范使用其他锁定机制。

第二个问题更难。 消除“全局锁”通常需要重新考虑应用程序架构,或更改为不需要外部锁定的不同数据结构。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接