It has been 628 days since the last update, the content of the article may be outdated.
什么是管程(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 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 @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 3 : dup 4 : astore_1 5 : monitorenter 6 : getstatic #3 9 : iconst_1 10 : iadd 11 : putstatic #3 14 : aload_1 15 : monitorexit 16 : goto 24 19 : astore_2 20 : aload_1 21 : monitorexit 22 : aload_2 23 : athrow 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 offset_delta = 19 locals = [ class "[Ljava/lang/String;" , class java /lang/Object ] stack = [ class java /lang/Throwable ] frame_type = 250 offset_delta = 4