synchronized与对象的Monitor

synchronized介绍

关于synchronized我们已经在《synchronized与Lock》一文中进行了比较全面的介绍。

当我们使用synchronized修饰方法名时,编译后会在方法名上生成一个ACC_SYNCHRONIZED标识来实现同步;当使用synchronized修饰代码块时,编译后会在代码块的前后生成monitorenter和monitorexit字节码来实现同步。

无论使用哪种方式实现,本质上都是对指定对象相关联的monitor的获取,只有获取到对象的monitor的线程,才可以执行方法或代码块,其他获取失败的线程会被阻塞,并放入同步队列中,进入BLOCKED状态。

监视器monitor

这篇文章的主角——monitor,终于出现了,首先我们看一下java的官方文档中对monitor的解释:

Synchronizationis built around an internal entity known as the intrinsic lock ormonitor lock. (The API specification often refers to this entity simplyas a “monitor.”),Every object has an intrinsic lock associated with it.By convention, a thread that needs exclusive and consistent access toan object’s fields has to acquire the object’s intrinsic lock beforeaccessing them, and then release the intrinsic lock when it’s done withthem.

(先根据官方文档解释一番)

在多线程访问共享资源的时候,经常会带来可见性和原子性的安全问题。为了解决这类线程安全的问题,Java提供了同步机制、互斥锁机制,这个机制保证了在同一问题内只有一个线程能访问共享资源。这个机制的保障来源于监视锁Monitor,每个对象都拥有自己的监视锁Monitor。

Monitor Object 设计模式

我们使用Monitor Object设计模式来解决这类问题:将被客户线程并发访问的对象定义为一个monitor对象。客户线程仅能通过monitor对象的同步方法才能使用monitor对象定义的服务。为了防止陷入竞争条件,在任一时刻只能有一个同步方法被执行。每一个monitor对象包含一个monitor锁,被同步方法用于串行访问对象的行为和状态。此外,同步方法可以根据一个或多个与monitor对象相关的monitor conditions来决定在何种情况下挂起或恢复它们的执行。

Java对于这样一个典型的模式做了很好的语言层面的封装,因此对于Java的开发者来说,很多关于该模式本身的东西被屏蔽掉了,如果希望从本质上对Monitor Object设计模式有一个更全面的认识,可以结合C++版本的Monitor Object设计模式。可参考此文:https://www.ibm.com/developerworks/cn/java/j-lo-synchronized/

结构

在Monitor Object模式中,主要有四种类型的参与者:

  • 监视者对象(Monitor Object):负责定义公共的接口方法,这些公共的接口方法会在多线程的环境下被调用执行;
  • 同步方法:这些方法是监视者对象锁定义。为了防止竞争条件,无论是否同时有多个线程并发调用同步方法,还是监视者对象含有多个同步方法,在任一时间内只有监视者对象的一个同步方法能够被执行。
  • 监视锁(Monitor Lock):每一个监视者对象都会拥有一把监视锁。
  • 监视条件(Monitor Condition):同步方法使用监视锁和监视条件来决定方法是否需要阻塞或重新执行。
执行时序图

认识Java Monitor Object

Java Monitor从两个方面来支持线程之间的同步,即:互斥执行与协作。Java使用对象锁(通过synchronized获得对象锁)保证工作在共享的数据集上的线程互斥执行,使用 notify/notifyAll/wait 方法来协同不同线程之间的工作。这些方法在Object类上被定义,会被所有的Java对象自动继承。

实质上,Java的Object类本身就是监视者对象,Java语言对于这样一个典型并发设计模式做了内建的支持。不过,在Java里,我们已经看不到C++中的区域锁与条件变量的概念了。下图很好地描述了Java Monitor的工作机理:

线程如果获得监视锁成功,将成为监视者对象的拥有者。在任一时刻内,监视者对象只属于一个活动线程(Owner)。拥有者线程可以调用wait方法自动释放监视锁,进入等待状态。

Java Monitor Object的实践

在这里,我们将使用监视者对象设计模式来解决一个实际问题。

这是一个典型的生产者消费者模式的问题。假设我们由一个固定长度的消息队列,该队列会被多个生产者消费者线程所操作,生产者线程负责将消息放入该队列,而消费者线程负责从该队列中取出消息。

Message Class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Message {
private static int OBJECT_COUNT = 0;
public int objectIndex;
Message() {
synchronized(Message.class) {
OBJECT_COUNT++;
objectIndex = OBJECT_COUNT;
}
}
@Override
public String toString() {
return "message["+objectIndex+"]";
}
}

MessageQueue Class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class MessageQueue {
private int messageCount;
private int maxMessages;
private Message[] buffer;
private int in = 0, out = 0;
public MessageQueue(int maxMessages) {
maxMessages = maxMessages;
messageCount = 0;
buffer = new Message[maxMessages];
}
synchronized boolean full() {
return isFull();
}
synchronized void put(Message msg) {
while(isFull()) {
try {
wait(); //release monitor lock, wait for space in the queue
} catch(InterruptedException e) {
// do something
} finally {
// do something
}
}
put(msg);
notifyAll();
}
synchronized Message get() {
while(isEmpty()) {
try {
wait();//release monitor lock, wait for message in the queue
} catch(InterruptedException e) {
// do something
} finally {
// do something
}
}
Message msg = get();
notifyAll();
return msg;
}
private boolean isEmpty() {
return messageCount == 0;
}
private boolean isFull() {
return messageCount == maxMessages;
}
private void put(Message msg) {
buffer[in] = msg;
in = (in + 1) % maxMessages;//避免越界
messageCount++;
}
private Message get() {
Message msg = buffer[out];
out = (out + 1) % maxMessages;
messageCount--;
return msg;
}
}

总结

在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 中对于 synchronized 的使用,后面一定要跟{}语句块,这在代码的书写上有些不灵活,而在C++中的,Scoped Lock默认就是保护当前的语句块,当然你也可以选择使用{}来显示声明。而且,使用 synchronized 所获得的对象锁,无法细粒度地区分是获得读锁还是写锁。

不过总的来说,Java的确简化了基于 Monitor Object 并发模式的开发。不过,我们应该意识到,并发的实际应用开发绝不会像 Java语法这么体现出来的简单,简洁。我们更应该看到并发应用程序本质的一些东西,这有利于帮助我们构建更加健壮的并发应用。