说一说管程(Monitor)及其在Java synchronized机制中的体现

一、什么是管程

管程首先由霍尔(C.A.R.Hoare)和汉森(P.B.Hansen)两位大佬提出,是一种并发控制机制,由编程语言来具体实现。它负责管理共享资源以及对共享资源的操作,并提供多线程环境下的互斥和同步,以支持安全的并发访问。“共享资源以及对共享资源的操作”在操作系统理论中称为critical section,即临界区。

管程能够保证同一时刻最多只有一个线程访问与操作共享资源(即进入临界区)。在临界区被占用时,其他试图进入临界区的线程都将等待。如果线程不能满足执行临界区逻辑的条件(比如资源不足),就会阻塞。阻塞的线程可以在满足条件时被唤醒,再次试图执行临界区逻辑。

用简单的比喻形象地解释下,将管程想象成一家只有一个服务窗口的银行,如下图所示。
在这里插入图片描述
来银行办事的多位客人首先都会进入大厅,但服务窗口只能同时为一位客人办理业务,其他人都在大厅等着。假设客人A办理业务的中途出了问题,如需要临时复印一下证件之类的,A就会去等待室,另外一位客人B去窗口办理业务。A复印完证件之后仍然在等待室待着,等B完事之后,再回去继续。当然,实际的管程(银行也一样)要复杂得多。

我们已经知道,操作系统原生提供了信号量(Semaphore)和互斥量(Mutex),开发者用它们也能实现与管程相同的功能,那为什么还需要管程呢?因为信号量和互斥量都是低级原语,使用它们时必须手动编写wait和signal逻辑,所以要特别小心。一旦wait/signal逻辑出错,分分钟造成死锁。管程就可以对开发者屏蔽掉这些细节,在语言内部实现,更加简单易用。

由上面的叙述可知,管程并不像它的名字所说的一样是个简单的程序,而是由以下3个元素组成:

  • 临界区;
  • 条件变量,用来维护因不满足条件而阻塞的线程队列。注意,条件由开发者在业务代码中定义,条件变量只起指示作用,亦即条件本身并不包含在条件变量内;
  • Monitor对象,维护管程的入口、临界区互斥量(即锁)、临界区和条件变量,以及条件变量上的阻塞和唤醒操作。

二、Mesa管程模型

管程的设计有Hansen、Hoare和Mesa三种模型。本文介绍Mesa管程模型,因为它比较流行,并且是Java采用的设计方案。我们可以将Mesa风格的管程看作是如下图的房间。
在这里插入图片描述
图中有两个条件变量a、b,它们对应的线程队列为a.q和b.q,另外还有一个入口队列e,它们分别占用一个房间。右下角的大房间即为临界区。该模型的执行流程如下:

  1. 多个线程进入管程的入口队列e,并试图获取临界区锁。获取到锁的线程进入临界区,其他线程仍然在e中。
  2. 通过外部条件来判断进入临界区的线程是否能执行操作,分为以下3、4两种情况。
  3. 如果不能执行,则调用wait原语,该线程阻塞,释放临界区的锁,离开临界区并根据条件进入a.q或者b.q。
  4. 如果能执行,那么在执行完毕后调用notify原语(相当于signal),唤醒a.q或b.q中的一个线程。执行完毕的线程释放锁,并离开管程的作用域。
  5. 被唤醒的线程进入队列e,返回第1步重新开始。

Mesa管程的特点是:线程由阻塞状态被唤醒之后不会立即执行,而是回到入口等待。相对地,Hoare管程在线程被唤醒后就会立即切换上下文,让被唤醒的线程先执行。后者的实现简单,但会触发更多的上下文切换操作,浪费CPU时间。前者的效率自然比较高,但带来的潜在问题是线程回到队列e后,原先满足的条件可能已经不再满足,必须重新检查。所以在Mesa管程模型下编写程序时,检查条件应该用while,而不是if:

while (!condition) {
    wait(a)
}

三、Java synchronized背后的管程

java.util.concurrent包出现之前(即JDK 1.5之前),Java仅由synchronized关键字和Object.wait()/notify()/notifyAll()方法来实现并发控制,JUC包出现之后才有了更加丰富的实现,如ReentrantLock等。下面粗略地研究一下较为基础的synchronized背后的事情,先来看示例代码。

public class SynchronizedExample {
    private final Object lockObj = new Object();
    private int data;
    private volatile boolean isAvailable = false;

    public void method1() {
        synchronized (this) {
            System.out.println("Synchronized block w/ this");
        }
    }

    public void method2() {
        synchronized (lockObj) {
            System.out.println("Synchronized block w/ lock object");
        }
    }
    
    public static synchronized void method3() {
        System.out.println("Synchronized static method");
    }
    
    public synchronized int get() {
        try {
            while (!isAvailable) {
                wait();
            }
        } catch (InterruptedException e) { 
            e.printStackTrace();
        }
        isAvailable = false;
        notifyAll();
        return data;
    }
    
    public synchronized void put(int data) {
        try {
            while (isAvailable) {
                wait();
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        this.data = data;
        isAvailable = true;
        notifyAll();
    }
}

这段代码展示了synchronized关键字的四种用法:使用this作为同步对象的同步代码块、使用其他对象作为同步对象的同步代码块、同步实例方法、同步静态方法。由synchronized关键字修饰的代码块和方法就是管程的临界区。另外,get()和put()方法利用wait()和notifyAll()实现了极简的同步生产者/消费者逻辑。

synchronized关键字总要有一个同步对象与其关联,如上面代码中的this和lockObj。特别地,在它修饰实例方法时,会隐式地使用this,修饰静态方法时则会隐式地使用this.class。这个同步对象——即java.lang.Object——就是管程的Monitor对象。

问题来了:Object是如何维护Monitor对象需要的许多信息的呢?

如果看官对HotSpot有一定了解的话,就会知道堆中的对象实例由对象头、实例数据和对齐填充3部分组成,而对象头又由Mark Word和类元数据指针2部分组成。Mark Word是一个非固定的数据结构,长度与JVM位数相同,用于存储对象自身的运行时数据,具体如下表所示。

在这里插入图片描述
注意标志位为10,即重量级锁定时,Mark Word会保存指向重量级锁的指针。在HotSpot代码中,是指向ObjectMonitor类型的指针。ObjectMonitor的构造方法如下所示。

  ObjectMonitor() {
    _header       = NULL;
    _count        = 0;
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL;
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;
  }

其中有几个非常重要的字段,有必要说明一下。

  • _owner:持有该ObjectMonitor的线程的指针;
  • _count:线程获取管程锁的次数;
  • _waiters:处于等待状态的线程数;
  • _recursions:管程锁的重入次数;
  • _EntryList:管程的入口线程队列(双向链表);
  • _WaitSet:处于等待状态的线程队列(双向链表);
  • _cxq:线程竞争管程锁时的队列(单向链表)。

其中,_EntryList就相当于Mesa管程模型中的队列e,而_WaitSet就相当于其中的队列a.q或者b.q。Object.wait()/notify()/notifyAll()三个方法也会直接映射到ObjectMonitor的同名方法。由此也可见,ObjectMonitor只有一个隐式的条件变量,及与其相关的线程队列。_EntryList、_WaitSet和_owner之间的关系如下图所示。

在这里插入图片描述
我们已经知道,synchronized代码块在字节码中会用monitorenter和monitorexit指令来包含,如下:

  public void method1();
    Code:
       0: aload_0
       1: dup
       2: astore_1
       3: monitorenter
       4: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
       7: ldc           #5                  // String Synchronized block w/ this
       9: invokevirtual #6                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      12: aload_1
      13: monitorexit
      14: goto          22
      17: astore_2
      18: aload_1
      19: monitorexit
      20: aload_2
      21: athrow
      22: return

monitorenter的逻辑在InterpreterRuntime::monitorenter()方法中,其源码如下。

IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
#ifdef ASSERT
  thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
  if (PrintBiasedLockingStatistics) {
    Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
  }
  Handle h_obj(thread, elem->obj());
  assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
         "must be NULL or an object");
  if (UseBiasedLocking) {
    // Retry fast entry if bias is revoked to avoid unnecessary inflation
    ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
  } else {
    ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
  }
  assert(Universe::heap()->is_in_reserved_or_null(elem->obj()),
         "must be NULL or an object");
#ifdef ASSERT
  thread->last_frame().interpreter_frame_verify_monitor(elem);
#endif
IRT_END

该方法会根据是否启用偏向锁(UseBiasedLocking)来决定是使用偏向锁(调用ObjectSynchronizer::fast_enter()方法)还是轻量级锁(调用ObjectSynchronizer::slow_enter()方法)。如果不能获取到锁,就会按偏向锁→轻量级锁→重量级锁的顺序膨胀,而重量级锁就是与ObjectMonitor(即管程)相关的锁。

在JDK 1.6之前,使用synchronized就意味着使用重量级锁,即直接调用ObjectSynchronizer::enter()方法。之所以称为“重量级”,是因为线程的阻塞和唤醒都需要OS在内核态和用户态之间转换。而JDK 1.6引入了偏向锁、轻量级锁、适应性自旋、锁粗化、锁消除等大量优化,synchronized的效率也变高了。鉴于本文已经有点长了,本意也不是想非常深入,所以源码级别的内容(包含ObjectMonitor的实现,synchronized的具体执行流程,JDK对锁的优化措施)会在今后分别写文章来说明。

猜你喜欢

转载自blog.csdn.net/Allen_Adolph/article/details/106709841