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

什么是管程

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

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

用简单的比喻形象地解释下,将管程想象成一家只有一个服务窗口的银行,如下图所示。

e624460c645c

来银行办事的多位客人首先都会进入大厅,但服务窗口只能同时为一位客人办理业务,其他人都在大厅等着。假设客人A办理业务的中途出了问题,如需要临时复印一下证件之类的,A就会去等待室,另外一位客人B去窗口办理业务。A复印完证件之后仍然在等待室待着,等B完事之后,再回去继续。当然,实际的管程(银行也一样)要复杂得多。

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

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

临界区;

条件变量,用来维护因不满足条件而阻塞的线程队列。注意,条件由开发者在业务代码中定义,条件变量只起指示作用,亦即条件本身并不包含在条件变量内;

Monitor对象,维护管程的入口、临界区互斥量(即锁)、临界区和条件变量,以及条件变量上的阻塞和唤醒操作。

Mesa管程模型

管程的设计有Hansen、Hoare和Mesa三种模型。本文介绍Mesa管程模型,因为它比较流行,并且是Java采用的设计方案。我们可以将Mesa风格的管程看作是如下图的房间。

e624460c645c

图中有两个条件变量a、b,它们对应的线程队列为a.q和b.q,另外还有一个入口队列e,它们分别占用一个房间。右下角的大房间即为临界区。该模型的执行流程如下:

多个线程进入管程的入口队列e,并试图获取临界区锁。获取到锁的线程进入临界区,其他线程仍然在e中。

通过外部条件来判断进入临界区的线程是否能执行操作,分为以下3、4两种情况。

如果不能执行,则调用wait原语,该线程阻塞,释放临界区的锁,离开临界区并根据条件进入a.q或者b.q。

如果能执行,那么在执行完毕后调用notify原语(相当于signal),唤醒a.q或b.q中的一个线程。执行完毕的线程释放锁,并离开管程的作用域。

被唤醒的线程进入队列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位数相同,用于存储对象自身的运行时数据,具体如下表所示。

e624460c645c

注意标志位为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之间的关系如下图所示。

e624460c645c

我们已经知道,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对锁的优化措施)会在今后分别写文章来说明。

民那晚安。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值