Java 中的监视器模式:使用监视器实现健壮的锁机制
也称为
- 同步块
监视器设计模式的意图
Java 中的监视器设计模式对于同步并发操作至关重要,它确保线程安全并防止竞态条件。
使用真实案例详细解释监视器模式
真实案例
想象一个共享的办公室打印机,需要多位员工使用。为了避免不同文档的页面混合在一起,打印机每次只能处理一项打印作业。这个场景类似于编程中的监视器设计模式。
在这个例子中,打印机代表共享资源,而员工类似于线程。系统设置了每个员工必须在开始打印作业之前请求访问打印机的机制。此系统确保一次只有一个员工(或“线程”)可以使用打印机,防止任何作业之间的重叠或干扰。打印作业完成后,下一个排队中的员工可以访问打印机。这种机制反映了监视器模式控制访问共享资源的方式,确保多个“线程”(员工)有序且安全地使用。
简单来说
监视器模式用于强制对数据的单线程访问。一次只允许一个线程执行监视器对象内的代码。
维基百科说
在并发编程(也称为并行编程)中,监视器是一种同步机制,它允许线程同时拥有互斥访问权限,以及等待(阻塞)某个条件变为假的能力。监视器还具有向其他线程发出信号的机制,以表明其条件已满足。
Java 中监视器模式的编程示例
监视器设计模式是一种在并发编程中使用的同步技术,以确保一次只有一个线程可以执行特定代码段。它是一种将同步原语(例如信号量或锁)包装并隐藏在对象方法中的方法。这种模式在可能发生竞态条件的情况下非常有用。
在 Bank
类示例中可以看到 Java 监视器设计模式。通过使用同步方法,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) {
// Only one thread can execute this method at a time due to the 'synchronized' keyword.
if (accounts[accountA] >= amount && accountA != accountB) {
accounts[accountB] += amount;
accounts[accountA] -= amount;
}
}
public synchronized int getBalance() {
// Only one thread can execute this method at a time due to the 'synchronized' keyword.
int balance = 0;
for (int account : accounts) {
balance += account;
}
return balance;
}
public synchronized int getBalance(int accountNumber) {
// Only one thread can execute this method at a time due to the 'synchronized' keyword.
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 中使用监视器模式
监视器设计模式应该在您拥有需要由多个线程或进程同时访问和操作的共享资源的情况下使用。这种模式在需要同步以防止竞态条件、数据损坏和不一致状态的情况下特别有用。以下是一些您应该考虑使用监视器模式的情况
共享数据:当您的应用程序涉及需要由多个线程访问和更新的共享数据结构、变量或资源时。监视器确保一次只有一个线程可以访问共享资源,从而防止冲突并确保数据一致性。
关键部分:当您有需要仅由一个线程一次执行的关键代码段时。关键部分是操作共享资源的代码部分,并发访问会导致问题。监视器有助于确保在任何给定时间只有一个线程可以执行关键部分。
线程安全:当您需要确保线程安全而不依赖于低级同步机制(例如锁和信号量)时。监视器提供更高层次的抽象,封装了同步和资源管理。
等待和信号:当您遇到线程需要等待某些条件满足才能继续执行的情况时。监视器通常包含线程等待特定条件的机制,以及其他线程在条件满足时通知它们的机制。
死锁预防:当您想通过提供一种结构化的方法来获取和释放对共享资源的锁来防止死锁时。监视器通过确保资源访问得到良好管理,有助于避免常见的死锁情况。
并发数据结构:当您实现并发数据结构(如队列、堆栈或哈希表)时,多个线程需要操作该结构,同时保持其完整性。
资源共享:当多个线程需要共享有限的资源时,例如数据库连接或网络套接字的访问权限。监视器可以帮助以受控的方式管理这些资源的分配和释放。
改进可维护性:当您希望将同步逻辑和共享资源管理封装在一个对象中时,可以改进代码组织,并使对并发相关代码的推理变得更容易。
但是,重要的是要注意,监视器模式可能并不适合所有并发场景。在某些情况下,其他同步机制(如锁、信号量或并发数据结构)可能更合适。此外,现代编程语言和框架通常提供更高层次的并发结构,抽象了低级同步的复杂性。
在应用监视器模式之前,建议仔细分析应用程序的并发要求,并选择最适合您需求的同步方法,同时考虑性能、复杂性和可用语言特性等因素。
Java 中监视器模式的实际应用
Java 中监视器设计模式的常见实现包括同步方法和块,以及并发数据结构(如 Vector
和 Hashtable
)。
监视器模式的优缺点
优点
- 确保互斥访问,防止竞态条件。
- 通过为资源访问提供清晰的结构,简化了线程管理的复杂性。
缺点
- 由于锁定开销,可能会导致性能下降。
- 如果设计不当,可能会导致死锁。
相关的 Java 设计模式
信号量:用于控制多个线程对公共资源的访问;监视器在其核心使用二元信号量概念。
互斥锁:另一种确保互斥访问的机制;监视器是一种更高级的结构,通常使用互斥锁实现。