说一说管程(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对锁的优化措施)会在今后分别写文章来说明。

民那晚安。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,015评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,262评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,727评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,986评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,363评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,610评论 1 219
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,871评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,582评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,297评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,551评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,053评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,385评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,035评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,079评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,841评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,648评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,550评论 2 270