并发编程:共享模型之管程

什么是管程(Monitor)

管程(Monitor)是一种并发编程的模型和技术,用于解决多线程之间的互斥访问和条件等待的问题。它是由荷兰计算机科学家 Edsger Dijkstra 在 1971 年提出的一种同步机制。

管程提供了一种方式让多个线程能够协调彼此的执行,确保在任意时刻只有一个线程可以访问共享资源,从而避免了线程间的竞争条件(Race Condition)和数据不一致的问题。

管程通常由以下几个要素组成:

互斥锁(Mutex):用于实现对共享资源的互斥访问,保证同一时刻只有一个线程可以进入管程进行操作。

条件变量(Condition Variable):用于实现线程间的条件等待和通知机制。线程可以在条件不满足的情况下等待,并在条件满足时被唤醒继续执行。

入口队列(Entry Queue):用于存放等待访问资源的线程队列。

管程内部方法:包括对资源的操作和对条件变量的操作等。

通过管程,可以将对共享资源的访问限制在管程的范围内,线程在进入管程之前需要获得管程的互斥锁,如果资源被占用,线程将进入入口队列等待;当某个线程完成对共享资源的操作后,会释放互斥锁并唤醒等待队列中的某个线程继续执行。

管程的主要优点是提供了一种高层次的抽象和封装,简化了多线程编程的复杂性,提供了一种结构化的方式来管理线程之间的同步和协作。它可以有效地避免死锁(Deadlock)和饥饿(Starvation)等并发编程中常见的问题。

举例

在多线程中有A、B两个线程同时需要修改某一个共享资源,线程A从内存中读取共享资源,但还未来得及提交修改,或者处于休眠状态;同时,线程B获取到同一个共享资源并进行了修改,然后将修改后的值放回内存中。接着,当线程A继续执行并将它的修改放回内存时,它会覆盖线程B之前的修改,从而导致最终的结果不一致

上述例子也就体现了,线程A和线程B同时竞争修改共享资源,其操作顺序可能会导致不一致的结果,因此我们可以采用同步机制,管程或锁机制来确保只有一个线程可以修改资源

Java体现资源争抢

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter++;
}
}, "t1");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 5000; i++) {
counter--;
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}",counter);
}

以上的结果可能为负数、正数或0,因为Java中对静态变量的自增或自减操作并不是原子性操作

临界区 Critical Section

  • 一个程序运行多个线程本身是没有问题的
  • 问题出在多个线程访问共享资源
    • 多个线程共享资源其实也没有问题
    • 在多个线程对共享资源读写操作时发生指令交错,就会出现问题
  • 一段代码块内如果存在对共享资源的多线程读写操作,称这段代码块为临界区 例如,下面代码中的临界区
1
2
3
4
5
6
7
8
9
10
11
static int counter = 0;
static void increment()
// 临界区
{
counter++;
}
static void decrement()
// 临界区
{
counter--;
}

竞态条件 Race Condition

多个线程在临界区内执行,由于代码的执行序列不同而导致结果无法预测,称之为发生了竞态条件

synchronized

为了避免临界区的竞态条件发生,有多种手段可以达到目的。

  • 阻塞式的解决方案:synchronized,Lock
  • 非阻塞式的解决方案:原子变量

synchronized 即俗称的【对象锁】,它采用互斥的方式让同一 时刻至多只有一个线程能持有【对象锁】,其它线程再想获取这个【对象锁】时就会阻塞住。这样就能保证拥有锁的线程可以安全的执行临界区内的代码,不用担心线程上下文切换(也就是线程切换)

注意

虽然 java 中互斥和同步都可以采用 synchronized 关键字来完成,但它们还是有区别的:

  • 互斥是保证临界区的竞态条件发生,同一时刻只能有一个线程执行临界区代码
  • 同步是由于线程执行的先后、顺序不同、需要一个线程等待其它线程运行到某个点

语法

1
2
3
4
synchronized(对象) // 线程1, 线程2(blocked)
{
临界区
}

解决

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
/**
* @Author: SerMs
* @Date: 2023/10/27 11 58
* @Email: 1839928782@qq.com
* @Blog: https://serms.top
**/

@Slf4j(topic = "c.Test04")
public class Test04 {
static int counter = 0;
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(() -> {
synchronized (Test04.class) {
for (int i = 0; i < 5000; i++) {
counter++;
}
}
}, "t1");
Thread t2 = new Thread(() -> {
synchronized(Test04.class){
for (int i = 0; i < 5000; i++) {
counter--;
}
}
}, "t2");
t1.start();
t2.start();
t1.join();
t2.join();
log.debug("{}",counter);
}
}

方法上的 synchronized

1
2
3
4
5
6
7
8
9
10
11
12
13
class Test{
public synchronized void test() {

}
}
等价于
class Test{
public void test() {
synchronized(this) {

}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
class Test{
public synchronized static void test() {
}
}
等价于
class Test{
public static void test() {
synchronized(Test.class) {

}
}
}

Monitor概念

Java 对象头

普通对象

1
2
3
4
5
|--------------------------------------------------------------|
| Object Header (64 bits) |
|------------------------------------|-------------------------|
| Mark Word (32 bits) | Klass Word (32 bits) |
|------------------------------------|-------------------------|

数组对象

1
2
3
4
5
|---------------------------------------------------------------------------------|
| Object Header (96 bits) |
|--------------------------------|-----------------------|------------------------|
| Mark Word(32bits) | Klass Word(32bits) | array length(32bits) |
|--------------------------------|-----------------------|------------------------|

其中 Mark Word 结构为

1
2
3
4
5
6
7
8
9
10
11
12
13
|-------------------------------------------------------|--------------------|
| Mark Word (32 bits) | State |
|-------------------------------------------------------|--------------------|
| hashcode:25 | age:4 | biased_lock:0 | 01 | Normal |
|-------------------------------------------------------|--------------------|
| thread:23 | epoch:2 | age:4 | biased_lock:1 | 01 | Biased |
|-------------------------------------------------------|--------------------|
| ptr_to_lock_record:30 | 00 | Lightweight Locked |
|-------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:30 | 10 | Heavyweight Locked |
|-------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|-------------------------------------------------------|--------------------|

64位虚拟机Mark Word

1
2
3
4
5
6
7
8
9
10
11
12
13
|--------------------------------------------------------------------|--------------------|
| Mark Word (64 bits) | State |
|--------------------------------------------------------------------|--------------------|
| unused:25 | hashcode:31 | unused:1 | age:4 | biased_lock:0 | 01 | Normal|
|--------------------------------------------------------------------|--------------------|
| thread:54 | epoch:2 | unused:1 | age:4 | biased_lock:1 | 01 | Biased |
|--------------------------------------------------------------------|--------------------|
| ptr_to_lock_record:62 | 00 | Lightweight Locked |
|--------------------------------------------------------------------|--------------------|
| ptr_to_heavyweight_monitor:62 | 10 | Heavyweight Locked |
|--------------------------------------------------------------------|--------------------|
| | 11 | Marked for GC |
|--------------------------------------------------------------------|--------------------|

上图所示,其实Monitor就是充当一个锁,只要是同一个对象,都会跟同一个Monitor相关联

  • Monitor 被翻译为监视器或管程
  • 每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针

Monitor 结构如下

  • 刚开始 Monitor 中 Owner 为 null
  • 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一 个 Owner
  • 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入 EntryList BLOCKED
  • Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的
  • 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,但条件不满足进入 WAITING 状态的线程,后面讲 wait-notify 时会分析

注意

  • synchronized 必须是进入同一个对象的 monitor 才有上述的效果
  • 不加 synchronized 的对象不会关联监视器,不遵从以上规则

Monitor原理之synchronize

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
static final Object lock = new Object();
static int counter = 0;
public static void main(String[] args) {
synchronized (lock) {
counter++;
}
}


//字节码
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=1
0: getstatic #2 // <- lock引用 (synchronized开始)
3: dup
4: astore_1 // lock引用 -> slot 1
5: monitorenter // 将 lock对象 MarkWord 置为 Monitor 指针
6: getstatic #3 // <- i
9: iconst_1 // 准备常数 1
10: iadd // +1
11: putstatic #3 // -> i
14: aload_1 // <- lock引用
15: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
16: goto 24
19: astore_2 // e -> slot 2
20: aload_1 // <- lock引用
21: monitorexit // 将 lock对象 MarkWord 重置, 唤醒 EntryList
22: aload_2 // <- slot 2 (e)
23: athrow // throw e
24: return
Exception table:
from to target type
6 16 19 any
19 22 19 any
LineNumberTable:
line 8: 0
line 9: 6
line 10: 14
line 11: 24
LocalVariableTable:
Start Length Slot Name Signature
0 25 0 args [Ljava/lang/String;
StackMapTable: number_of_entries = 2
frame_type = 255 /* full_frame */
offset_delta = 19
locals = [ class "[Ljava/lang/String;", class java/lang/Object ]
stack = [ class java/lang/Throwable ]
frame_type = 250 /* chop */
offset_delta = 4