【Java设计模式】监视器模式

一、概述

Java中的监视器设计模式对于同步并发操作至关重要,确保线程安全并防止竞争条件。

二、详细解释及实际示例

  1. 实际示例:
    • 想象一下几个员工需要使用的共享办公室打印机。打印机一次只能处理一个打印作业,以避免不同文档的页面混淆。这个场景类似于编程中的监视器设计模式。
    • 在这个例子中,打印机代表共享资源,员工类似于线程。建立了一个系统,每个员工在开始打印作业之前必须请求访问打印机。这个系统确保一次只有一个员工(或“线程”)可以使用打印机,防止作业之间的任何重叠或干扰。一旦一个打印作业完成,队列中的下一个员工可以访问打印机。这种机制反映了监视器模式控制对共享资源访问的方式,确保多个“线程”(员工)有序和安全地使用。
  2. 通俗解释:
    • 监视器模式用于强制对数据进行单线程访问。一次只允许一个线程在监视器对象内执行代码。
  3. 维基百科解释:
    • 在并发编程(也称为并行编程)中,监视器是一种同步构造,它允许线程具有互斥和等待(阻塞)某个条件变为假的能力。监视器还具有一种机制,用于向其他线程发出其条件已满足的信号。

三、Java中监视器模式的编程示例

监视器设计模式是并发编程中使用的一种同步技术,确保一次只有一个线程可以执行特定的代码段。它是一种将同步原语(如信号量或锁)包装和隐藏在对象的方法中的方法。这种模式在可能发生竞争条件的情况下非常有用。
Java监视器设计模式可以在Bank类的示例中看到。通过使用同步方法,Bank类确保在任何给定时间只有一个线程可以执行交易,说明了监视器模式在实际应用中的有效使用。
以下是Bank类的简化版本,并带有额外的注释:

public class Bank {
    @Getter
    private final int[] accounts;
    public Bank(int accountNum, int baseAmount) {
        accounts = new int[accountNum];
        Arrays.fill(accounts, baseAmount);
    }
    public synchronized void transfer(int accountA, int accountB, int amount) {
        // 由于“synchronized”关键字,一次只能有一个线程执行此方法。
        if (accounts[accountA] >= amount && accountA!= accountB) {
            accounts[accountB] += amount;
            accounts[accountA] -= amount;
        }
    }
    public synchronized int getBalance() {
        // 由于“synchronized”关键字,一次只能有一个线程执行此方法。
        int balance = 0;
        for (int account : accounts) {
            balance += account;
        }
        return balance;
    }
    public synchronized int getBalance(int accountNumber) {
        // 由于“synchronized”关键字,一次只能有一个线程执行此方法。
        return accounts[accountNumber];
    }
}

在Main类中,创建了多个线程来对银行账户进行交易。作为监视器的Bank类确保这些交易以线程安全的方式执行。

public class Main {
    private static final int NUMBER_OF_THREADS = 5;
    private static final int BASE_AMOUNT = 1000;
    private static final int ACCOUNT_NUM = 4;
    public static void runner(Bank bank, CountDownLatch latch) {
        try {
            SecureRandom random = new SecureRandom();
            Thread.sleep(random.nextInt(1000));
            for (int i = 0; i < 1000000; i++) {
                bank.transfer(random.nextInt(4), random.nextInt(4), random.nextInt(0, BASE_AMOUNT));
            }
            latch.countDown();
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
    public static void main(String[] args) throws InterruptedException {
        var bank = new Bank(ACCOUNT_NUM, BASE_AMOUNT);
        var latch = new CountDownLatch(NUMBER_OF_THREADS);
        var executorService = Executors.newFixedThreadPool(NUMBER_OF_THREADS);
        for (int i = 0; i < NUMBER_OF_THREADS; i++) {
            executorService.execute(() -> runner(bank, latch));
        }
        latch.await();
    }
}

在这个例子中,Bank类是监视器,transfer方法是需要以互斥方式执行的关键部分。Java中的synchronized关键字用于实现监视器模式,确保一次只有一个线程可以执行transfer方法。

四、何时在Java中使用监视器模式

当您有需要由多个线程或进程同时访问和操作的共享资源时,应该使用监视器设计模式。这种模式在需要同步以防止竞争条件、数据损坏和不一致状态的场景中特别有用。以下是一些应该考虑使用监视器模式的情况:

  1. 共享数据:当您的应用程序涉及需要由多个线程访问和更新的共享数据结构、变量或资源时。监视器确保一次只有一个线程可以访问共享资源,防止冲突并确保数据一致性。
  2. 关键部分:当您有需要一次只由一个线程执行的关键代码段时。关键部分是操作共享资源的代码部分,并发访问可能会导致问题。监视器有助于确保在任何给定时间只有一个线程可以执行关键部分。
  3. 线程安全:当您需要确保线程安全而不仅仅依赖于低级同步机制如锁和信号量时。监视器提供了一个更高层次的抽象,封装了同步和资源管理。
  4. 等待和信号:当您有线程需要等待某些条件满足后才能继续执行的场景时。监视器通常包括线程等待特定条件的机制,以及其他线程在条件满足时通知它们的机制。
  5. 死锁预防:当您想要通过提供一种结构化的方式来获取和释放共享资源上的锁来预防死锁时。监视器通过确保资源访问得到良好管理,帮助避免常见的死锁场景。
  6. 并发数据结构:当您实现并发数据结构,如队列、栈或哈希表时,其中多个线程需要在维护其完整性的同时操作该结构。
  7. 资源共享:当多个线程需要共享有限的资源,如数据库连接或网络套接字的访问时。监视器可以帮助以受控方式管理这些资源的分配和释放。
  8. 提高可维护性:当您希望将同步逻辑和共享资源管理封装在一个对象中,提高代码组织并使并发相关代码更容易推理时。
    然而,需要注意的是,监视器模式可能并不适合所有并发场景。在某些情况下,其他同步机制如锁、信号量或并发数据结构可能更合适。此外,现代编程语言和框架通常提供更高层次的并发构造,抽象掉了低级同步的复杂性。
    在应用监视器模式之前,建议彻底分析您的应用程序的并发需求,并选择最适合您需求的同步方法,考虑性能、复杂性和可用的语言特性等因素。

五、监视器模式在Java中的实际应用

Java中监视器设计模式的常见实现包括同步方法和块,以及并发数据结构如Vector和Hashtable。

六、监视器模式的好处和权衡

好处:

  1. 确保互斥,防止竞争条件。
  2. 通过为资源访问提供清晰的结构,简化了线程管理的复杂性。

权衡:

  1. 由于锁定开销,可能导致性能下降。
  2. 如果设计不当,可能会导致死锁。

七、源码下载

监视器模式示例代码下载

Logo

北京人形旗下天工造物具身智能开源社区,聚焦具身天工与慧思开物两大平台

更多推荐