本文从典型的 Monitor Object 设计模式入手,从一个新的视角,来探讨 Java 语言的同步机制。
<!--START RESERVED FOR FUTURE USE INCLUDE FILES--><!-- include java script once we verify teams wants to use this and it will work on dbcs and cyrillic characters -->
<!--END RESERVED FOR FUTURE USE INCLUDE FILES-->
本文将从两个方面进行阐述:
- 使
用 C++ 语言来描述 Monitor Object 设计模式。Java 对于这样一个典型的模式做了很好的语言层面的封装,因此对于 Java
的开发者来说,很多关于该模式本身的东西被屏蔽掉了。本文试图使用 Native C++ 语言,帮助读者从本质上对 Monitor object
设计模式有一个更全面的认识。
- 结合 C++ 版本的 Monitor Object 设计模式,引领读者对于 Java 同步机制有一个更深刻的认识,帮助读者正确有效地使用 Java 同步机制。
预备知识
在开始正式讨论之前,需要了解一些预备知识。
什么是 RAII
资
源获取即初始化(RAII, Resource Acquisition Is Initialization)是指,在一个对象的构造函数中获得资源
,
并且在该对象的析构函数中释放它。这个资源可以是对象、内存、文件句柄或者其它类型。实现这种功能的类,我们就说它采用了资源获取即初始化(RAII)的
方式。 RAII 是一种很典型的语言惯用法,被很多的 OO 语言所使用,下面是 C++ 的例子。
清单 1. RAII Using C++ class Raii { public: // Store a pointer to the resource and initialize the resource. Raii(Resource &resource) :m_pRes (&resource){ m_pRes->initialize (); } // Close the resource when the execution goes out of scope. virtual ~Raii() { m_pRes->close (); } private: // Pointer to the resource we're managing. Resource *m_pRes; // ... maybe need disallow copying and assignment ... };
|
使
用 RAII 的好处是:由于析构函数由系统自动调用,这样可以帮助我们自动地隐式释放我们所获取的资源。事情上,我们熟知的很多 c++
技术都用到了这一设计模式,比如:智能指针 (Smart Pointer),以及我们接下来要讨论的范围锁 (Scoped Lock) 。
不同于 C++,Java 对象没有析构函数,Java System 提供了 GC 来管理内存资源。而对于像数据库连接,Sockets 这样类型的资源, Java 提供了 finalize() 来处理。但是,请注意,Java 的 finalizer 与 C++ 的析构函数是不同的,finalize() 函数由 GC 异步地在某个恰当的时候调用,我们不能等同地使用 finalize() 来实现 C++ 里的 RAII 。通常的做法是使用 Java 提供的 finally 语句块。
清单 2. RAII Using Java MyResource res = null; try { res = new MyResource(); // Use the resource } finally { //At exit point, close the resource. if (res != null) { res.close(); } }
|
什么是区域锁 (Scoped Lock)
区域锁是指线程执行进入一个区域时,一个锁将自动被获取,当该线程执行离开这个区域时,这个锁将被自动释放。 C++ 区域锁的实现使用了 RAII 技术 , 实现如下。
清单 3. Scoped Lock Using C++ template <class LOCK> class Guard { public: // Store a pointer to the lock and acquire the lock. Guard (LOCK &lock) :m_pLlock (&lock), m_bOwner (false) { m_pLlock->acquire (); m_bOwner = true; } // Release the lock when the guard goes out of scope, // but only if <acquire> succeeded. virtual ~Guard () { if (m_bOwner) m_pLlock->release (); } private: // Pointer to the lock we're managing. LOCK *m_pLlock; // Records if the lock is held by this object. bool m_bOwner; // ... maybe need disallow copying and assignment ... }; |
Guard 是一个模板类,LOCK 类型指的是对操作系统提供的线程锁的抽象,比如,在 Windows 平台上,LOCK 可以是对 CRITICAL_SECTION 的封装。
那么对于 Java,怎么实现区域锁呢?不必担心,Java 对于区域锁模式在语言层面上已经做了封装,所以对于 Java 开发者来说,不必像 C++ 这样来开发自己的区域锁类,这就是我们所熟知的 synchronized 关键字。
清单 4. Scoped Lock Using Java public int scopedLockSample() { synchronized(this) { try { //do some work… } catch( MyException1 e) { //no need release lock explicitly return -1; } catch( MyException2 e) { //no need release lock explicitly return -2; } //other exceptions handling... } return 0; } |
synchronized 保证在进入该区域后,获得对象锁,不管最终该函数从哪里退出,该对象锁都会被正确释放。
什么是条件变量 (Condition Variables)
条
件变量通常被一个线程用于使自己等待,直到一个涉及共享数据的条件表达式到达特定的状态。当另外的协作线程指示共享数据的状态已发生变化,调度器就唤醒在
该条件变量上挂起的线程。于是新唤醒的线程重新对它的条件表达式进行求值,如果共享数据已到达合适状态,就恢复处理。以下是条件变量的 C++ 实现。
清单 5. Thread Condition Using C++ class Thread_Condition { public: // Initialize the condition variable and associate it with the specified lock. Thread_Condition (const Thread_Mutex &m) :m_obMutex(m) { cond_init (&cond_, USYNC_THREAD, 0); } // Destroy the condition variable. virtual ~Thread_Condition () { cond_destroy (&cond_); } // Wait for the <Thread_Condition> to be notified // or until <timeout> has elapsed. If <timeout> == 0 then wait indefinitely. void wait (Time_Value *timeout = 0) { cond_timedwait(&cond_, &m_obMutex.m_lock,timeout == 0?0:timeout->msec ()); } // Notify one thread waiting on <Thread_Condition>. void notify () { cond_signal (&cond_); } // Notify all threads waiting on <Thread_Condition>. void notify_all () { cond_broadcast (&cond_); } private: // Solaris condition variable. cond_t cond_; // Reference to mutex lock. const Thread_Mutex &m_obMutex; }; |
Thread_Condition
的实现与操作系统提供的 API 密切相关,以上的例子是基于 Solaris condition variable API
的面向对象的封装。另外,这里的 Thread_Mutex 类型是对操作系统提供的线程锁的面向对象的封装 (Thread_Mutex 类型就是
Guard 模板参数 LOCK 所指向的类型 ) 。
而对于 Java,问题就变得简单很多,你不需要去封装自己的条件变量类,Java 的根类 Object 提供了 wait/notify/notifyAll 方法给开发者,很容易使用,这个我们在后面的讨论中会看到。
Monitor Object 设计模式 C++ 描述
我们将从以下几个方面来讨论 Monitor Object 模式。
问题描述
我们在开发并发的应用时,经常需要设计这样的对象,该对象的方法会在多线程的环境下被调用,而这些方法的执行都会改变该对象本身的状态。为了防止竞争条件 (race condition) 的出现,对于这类对象的设计,需要考虑解决以下问题:
- 在任一时间内,只有唯一的公共的成员方法,被唯一的线程所执行。
-
对于对象的调用者来说,如果总是需要在调用方法之前进行拿锁,而在调用方法之后进行放锁,这将会使并发应用编程变得更加困难。合理的设计是,该对象本身确保任何针对它的方法请求的同步被透明的进行,而不需要调用者的介入。
-
如果一个对象的方法执行过程中,由于某些条件不能满足而阻塞,应该允许其它的客户端线程的方法调用可以访问该对象。
我
们使用 Monitor Object 设计模式来解决这类问题:将被客户线程并发访问的对象定义为一个 monitor 对象。客户线程仅仅通过
monitor 对象的同步方法才能使用 monitor 对象定义的服务。为了防止陷入竞争条件,在任一时刻只能有一个同步方法被执行。每一个
monitor 对象包含一个 monitor 锁,被同步方法用于串行访问对象的行为和状态。此外,同步方法可以根据一个或多个与 monitor
对象相关的 monitor conditions 来决定在何种情况下挂起或恢复他们的执行。
结构
在 Monitor Object 模式中,主要有四种类型的参与者:
-
监视者对象 (Monitor Object): 负责定义公共的接口方法,这些公共的接口方法会在多线程的环境下被调用执行。
- 同步方法:这些方法是监视者对象所定义。为了防止竞争条件,无论是否同时有多个线程并发调用同步方法,还是监视者对象含有多个同步方法,在任一时间内只有监视者对象的一个同步方法能够被执行。
- 监视锁 (Monitor Lock): 每一个监视者对象都会拥有一把监视锁。
- 监视条件 (Monitor Condition): 同步方法使用监视锁和监视条件来决定方法是否需要阻塞或重新执行。
执行序列图
在监视者对象模式中,在参与者之间将发生如下的协作过程:
1、
同步方法的调用和串行化。当客户线程调用监视者对象的同步方法时,必须首先获取它的监视锁。只要该监视者对象有其他同步方法正在被执行,获取操作便不会成
功。在这种情况下,客户线程将被阻塞直到它获取监视锁。当客户线程成功获取监视锁后,进入临界区,执行方法实现的服务。一旦同步方法完成执行,监视锁会被
自动释放,目的是使其他客户线程有机会调用执行该监视者对象的同步方法。
2、同步方法线程挂起。如果调用同步方法的客户线程必须被阻塞或是有其他原因不能立刻进行,它能够在一个监视条件上等待,这将导致该客户线程暂时释放监视锁,并被挂起在监视条件上。
3、监视条件通知。一个客户线程能够通知一个监视条件,目的是为了让一个前期使自己挂起在一个监视条件上的同步方法线程恢复运行。
4、同步方法线程恢复。一旦一个早先被挂起在监视条件上的同步方法线程获取通知,它将继续在最初的等待监视条件的点上执行。在被通知线程被允许恢复执行同步方法之前,监视锁将自动被获取。图 1 描述了监视者
对象的动态特性。
图 1. Monitor Object Sequence Diagram.示例
在本节中,我们将使用监视者对象设计模式来解决一个实际的问题。
这是一个典型的生产者 / 消费者模式问题。假定我们有一个固定长度的消息队列,该队列会被多个生产者 / 消费者线程所操作,生产者线程负责将消息放入该队列,而消费者线程负责从该对列中取出消息。
清单 6. Message_Queue.h class Message_Queue { public: enum { MAX_MESSAGES = 100/* ... */ }; // The constructor defines the maximum number // of messages in the queue. This determines when the queue is 'full.' Message_Queue(size_t max_messages = MAX_MESSAGES); virtual ~Message_Queue(); // Put the <Message> at the tail of the queue. // If the queue is full, block until the queue is not full. /* synchronized */ void put (const Message &msg); // Get the <Message> from the head of the queue // and remove it. If the queue is empty, block until the queue is not empty. /* synchronized */ Message get(); // True if the queue is empty, else false. /* synchronized */ bool empty () const; // True if the queue is full, else false. /* synchronized */ bool full () const; private: // Put the <Message> at the tail of the queue, and // get the <Message> at its head, respectively. // Note that, the internal methods are not synchronized. void put_i (const Message &msg); Message get_i (); // True if the queue is empty, else false. bool empty_i () const; // True if the queue is full, else false. bool full_i () const; private: // Internal Queue representation omitted, could be a // circular array or a linked list, etc.. ... // Current number of <Message>s in the queue. size_t message_count_; // The maximum number <Message>s that can be // in a queue before it's considered 'full.' size_t max_messages_; // Monitor lock that protects the queue's // internal state from race conditions during concurrent access. mutable Thread_Mutex monitor_lock_; // Condition variable used in conjunction with <monitor_lock_> to make // synchronized method threads wait until the queue is no longer empty. Thread_Condition not_empty_; // Condition variable used in conjunction with <monitor_lock_> to make // synchronized method threads wait until the queue is no longer full. Thread_Condition not_full_; }; |
清单 7. Message_Queue.cpp #include "Message_Queue.h" Message_Queue::Message_Queue (size_t max_messages) :not_full_(monitor_lock_), not_empty_(monitor_lock_), max_messages_(max_messages), message_count_(0) { } bool Message_Queue::empty () const { Guard<Thread_Mutex> guard (monitor_lock_); return empty_i (); } bool Message_Queue::full () const { Guard<Thread_Mutex> guard (monitor_lock_); return full_i (); } void Message_Queue::put (const Message &msg) { // Use the Scoped Locking idiom to acquire/release the < monitor_lock_> upon // entry/exit to the synchronized method. Guard<Thread_Mutex> guard (monitor_lock_); // Wait while the queue is full. while (full_i ()) { // Release < monitor_lock_> and suspend the // calling thread waiting for space in the queue. // The <monitor_lock_> is reacquired automatically when <wait> returns. not_full_.wait (); } // Enqueue the <Message> at the tail. put_i (msg); // Notify any thread waiting in <get> that the queue has at least one <Message>. not_empty_.notify (); } // Destructor of <guard> releases <monitor_lock_>. Message Message_Queue::get () { // Use the Scoped Locking idiom to acquire/release the <monitor_lock_> upon // entry/exit to the synchronized method. Guard<Thread_Mutex> guard (monitor_lock_); // Wait while the queue is empty. while (empty_i ()) { // Release <monitor_lock_> and suspend the // calling thread waiting for a new <Message> to // be put into the queue. The <monitor_lock_> is // reacquired automatically when <wait> returns. not_empty_.wait (); } // Dequeue the first <Message> in the queue and update the <message_count_>. Message m = get_i (); // Notify any thread waiting in <put> that the // queue has room for at least one <Message>. not_full_.notify (); return m; } // Destructor of <guard> releases <monitor_lock_>. bool Message_Queue::empty_i () const { return message_count_ == 0; } bool Message_Queue::full_i () const { return message_count_ == max_messages_; } Message_Queue::~Message_Queue() { }
|
|
Monitor Object Java 实践
认识 Java Monitor Object
Java
Monitor 从两个方面来支持线程之间的同步,即:互斥执行与协作。 Java 使用对象锁 ( 使用 synchronized 获得对象锁 )
保证工作在共享的数据集上的线程互斥执行 , 使用 notify/notifyAll/wait 方法来协同不同线程之间的工作。这些方法在
Object 类上被定义,会被所有的 Java 对象自动继承。
实质上,Java 的 Object
类本身就是监视者对象,Java 语言对于这样一个典型并发设计模式做了内建的支持。不过,在 Java 里,我们已经看不到了我们在 C++
一节所讨论的区域锁与条件变量的概念。下图很好地描述了 Java Monitor 的工作机理。
图 2. Java Monitor线程如果获得监视锁成功,将成为该监视者对象的拥有者。在任一时刻内,监视者对象只属于一个活动线程 (Owner) 。拥有者线程可以调用 wait 方法自动释放监视锁,进入等待状态。
示例
在本节,我们将用 Java Monitor 来重新解决用 C++ 实现的生产者 / 消费者模式问题。
清单 8. Message Class public class Message { private static int OBJ_COUNT = 0; public int obj_index_; Message(){ synchronized(Message.class) { OBJ_COUNT++; obj_index_ = OBJ_COUNT; } }
@Override public String toString() { return "message["+obj_index_+"]"; } }
|
清单 9. MessageQueue Class public class MessageQueue { private int message_count_; private int max_messages_; private Message[] buffer_;
private int in_ = 0, out_ = 0; public MessageQueue(int max_messages) { max_messages_ = max_messages; message_count_ = 0; buffer_ = new Message[max_messages_]; }
synchronized boolean full () { return full_i (); } synchronized void put (Message msg) { while (full_i ()) { try { System.out.println("thread["+ Thread.currentThread().getId()+ "]"+ "release monitor lock, wait for space in the queue"); wait(); } catch (InterruptedException e) { //do something. } finally { //do something. } }//end while. put_i(msg); notifyAll(); } synchronized Message get() { while (empty_i ()) { try { System.out.println("thread["+ Thread.currentThread().getId()+ "]"+ "release monitor lock, wait for message in the queue"); wait(); } catch (InterruptedException e) { //do something. } finally { //do something. } }//end while. Message m = get_i (); notifyAll(); return m; } private boolean empty_i () { return message_count_ == 0; } private boolean full_i () { return message_count_ == max_messages_; } private void put_i (Message msg) { System.out.println("thread ["+ Thread.currentThread().getId()+ "] put message <"+ msg+ ">" + "to the queue"); buffer_[in_] = msg; in_ = (in_ + 1) % max_messages_; message_count_++; } private Message get_i() { Message msg = buffer_[out_]; out_= (out_ + 1) % max_messages_; message_count_--; System.out.println("thread ["+ Thread.currentThread().getId()+ "] get message <"+ msg+ ">" + "from the queue"); return msg; } }
|
在 Java 的示例中,没有放更多的注释,希望读者通过对照 C++ 的示例,来阅读理解这里的 Java 代码。可以看到,使用 Java 的版本代码简洁了很多。另外,这里提供的 Java 代码,稍作修改,是直接可以作为独立的 Java 程序运行的。
总结
我们对比一下 Monitor Object 设计模式的 C++ 版本与 Java 版本,做出如下的总结。
在
Java 的版本中,我们不需要亲自开发 Scoped Lock,Thread Condition 类,Java
语言给我们提供了内建的支持,我们很容易使用 synchronized, wait/notify 这些 Java 特性来构建基于 Monitor
Object 模式的应用。而缺点是:缺乏一些必要的灵活性。比如 : 在 Java 的版本中,我们并不能区分出 not empty 与 not
full 这两个条件变量,所以我们只能使用 notifyAll 来通知所有等待者线程,而 C++
版本使用了不同的通知唤醒:not_full_.notify 与 not_empty_.notify 。同样,在 Java 中对于
synchrnonized 的使用,后面一定要跟 {} 语句块,这在代码的书写上有些不灵活,而在 C++ 中的,Scoped Lock
默认就是保护当前的语句块,当然你也可以选择使用 {} 来显式声明。而且,使用 synchroninzed
所获得的对象锁,无法细粒度地区分是获得读锁还是写锁。
不过总的来说,Java 的确简化了基于 Monitor
Object 并发模式的开发。不过,我们应该意识到,并发的实际应用开发决不会像 Java
语法这么体现出来的简单,简洁。我们更应该看到并发应用程序本质的一些东西,这有利于帮助我们构建更加健壮的并发应用。
参考资料
关于作者
|
|
|
李三红任职于 IBM CDL,负责 Lotus Notes 产品研发。 |
分享到:
相关推荐
同步方法是监视者对象所定义的,为了防止竞争条件,无论是否同时有多个线程并发调用同步方法,还是监视者对象含有多个同步方法,在任一时间内只有监视者对象的一个同步方法能够被执行。监视锁是每一个监视者对象都会...
Java提供了两种主要的同步机制:同步代码块和同步方法。这两种机制都可以用来防止多个线程同时访问同一段代码,但它们的使用方式和应用场景有所不同。本文将详细探讨Java中的同步代码块和同步方法的区别,包括它们的...
### Java 并发基石:同步机制的全面解析 #### Java 的并发编程背景 Java作为一种广泛应用的编程语言,自1995年由Sun Microsystems(现属于Oracle公司)发布以来,已经发展成为一种支持跨平台性、面向对象编程、多...
##### 2.2 Java同步机制的局限性 Java语言提供了一系列用于同步的机制,如`synchronized`关键字、`wait()`和`notify()`等方法。尽管这些机制在一定程度上能够满足基本的同步需求,但在实际应用中存在以下局限性: ...
编译试验:后缀式的实现应该忽视的 AIX 命令 探索 Eclipse 的插件签名 探索 Java 同步机制 可以用PHP编写的30个游戏脚本 如何保证 Web 应用安全
了解并熟练掌握Java的等待唤醒机制对于编写高效的并发程序至关重要,它可以帮助我们解决多线程环境中的同步和通信问题。通过实践和理解这些基本原理,可以进一步探索更高级的并发工具,如`Condition`接口,`...
本文将围绕"手机探索者同步程序"的源代码进行深入探讨,主要涉及的技术领域包括同步机制、Linux操作系统以及与之相关的编程实践。 首先,我们要理解手机同步的核心概念。同步是指将手机中的数据(如联系人、日历、...
在Java中,`synchronized`关键字是最常用的同步机制之一,但它并不支持锁的公平性和非公平性选择。为了更细粒度地控制锁的行为,Java提供了`Lock`接口及其实现类,如`ReentrantLock`,允许开发者自定义锁的行为。 -...
本文将探索Java编程语言的概述、特点、应用和异常处理机制等方面。 Java编程语言概述 Java编程语言是计算机开发过程中常用的语言,它的出现推动了计算机软件开发的发展。Java语言可以单独地在一台电脑中运行,甚至...
AQS作为基础框架,支持了多种同步机制的实现,例如LatchBarrierBlockingQueue等。AQS通过内部类Sync映射所有同步器调用,维护资源状态的可用性最后,文档提供了AQS源码的初步分析,突出了其设计和实现的关键部分,如...
同时,Java的异常处理、多线程编程、并发模型和同步机制也是重要的学习内容。 除此之外,Java集合框架、I/O流、网络编程、反射机制、泛型、注解等特性都是Java2深度探索的重要组成部分。通过学习这些内容,开发者...
我们需要了解线程的基本概念、同步机制(synchronized、Lock)以及线程池的使用。 4. **Spring框架**:Spring是Java企业级应用开发中最流行的框架,它提供了依赖注入(DI)和面向切面编程(AOP)等功能,简化了开发...
Java提供了多种同步机制,如`synchronized`关键字、`wait()`, `notify()`, `notifyAll()`方法以及`Lock`接口(如`ReentrantLock`)等。 11_线程同步_7_生产者消费者问题.avi 文件可能讲解了一个经典的并发设计模式...
### 无线Java编程探索 #### 摘要与引言 本文主要探讨了无线Java编程技术,特别是针对移动设备如手机等的编程方法。随着移动通信技术的发展,手机已不仅仅局限于传统的语音通信功能,而是逐渐演变成了一种多功能的...
随着对基础知识的掌握,笔记会进一步带你探索Java集合框架,包括数组列表、链表、队列、栈、映射等数据结构,以及它们在实际问题中的应用。此外,多线程编程也是Java的一大亮点,笔记会介绍线程的创建与同步机制,如...
Java是一种广泛使用的面向对象的编程语言,以其跨平台、高性能和强大的库支持而闻名。本教程将深入探讨Java的基础知识,旨在...通过学习这些内容,你可以建立坚实的Java编程基础,为进一步探索Java世界打下坚实的基础。
考虑线程安全问题,选择合适的同步机制。 考虑性能问题,避免在高并发环境下产生性能瓶颈。 考虑序列化问题,防止通过反序列化创建新的实例。 考虑代码的可读性和可维护性,选择简洁且易于理解的实现方式。 总结来说...
书中通过实例展示了如何使用实时线程(Real-Time Threads)、内存模型(Memory Models)、同步机制(Synchronization)、实时时钟API(Real-Time Clock API)、异步事件处理(Asynchronous Event Processing)等技术...
因此,需要确保线程同步机制得到适当应用,以保证数据的完整性和一致性。 5. 资源关闭和异常处理:在多线程环境下,网络资源如socket和流需要正确关闭,以避免资源泄露。同时,需要妥善处理网络异常和I/O异常,保证...
Java提供了丰富的并发工具,如线程、守护线程、线程池、同步机制(synchronized关键字、Lock接口)、并发集合(ConcurrentHashMap、CopyOnWriteArrayList)等。理解和熟练运用这些工具,可以编写出高效、安全的多...