Monitor 原理和 Synchronized 原理

Monitor 原理和 Synchronized 原理

Monitor 原理

Monitor 被翻译为监视器或管程。

每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的 Mark Word 中就被设置指向 Monitor 对象的指针。(如果关于对象头不清晰的可以看看我上一篇关于对象头的博客)。Java 使用对象锁 ( 使用 synchronized 获得对象锁 ) 保证工作在共享的数据集上的线程互斥执行。

Monitor 结构如下:

image-20211218203705367

image-20211218203801357

  1. 刚开始 Monitor 中 Owner 为 null
  2. 当 Thread-2 执行 synchronized(obj) 就会将 Monitor 的所有者 Owner 置为 Thread-2,Monitor中只能有一个 Owner
  3. 在 Thread-2 上锁的过程中,如果 Thread-3,Thread-4,Thread-5 也来执行 synchronized(obj),就会进入 EntryList BLOCKED
  4. Thread-2 执行完同步代码块的内容,然后唤醒 EntryList 中等待的线程来竞争锁,竞争的时是非公平的
  5. 图中 WaitSet 中的 Thread-0,Thread-1 是之前获得过锁,Owner 线程发现条件不满足,调用 wait 方法,即可进入 WaitSet 变为 WAITING 状态。(BLOCKED 和 WAITING 的线程都处于阻塞状态,不占用 CPU 时间片)
  6. BLOCKED 线程会在 Owner 线程释放锁时唤醒,WAITING 线程会在 Owner 线程调用 notify 或 notifyAll 时唤醒,但唤醒后并不意味者立刻获得锁,仍需进入 EntryList 重新竞争锁的使用权。

Synchronized 原理

synchronize 的三种应用方式

  • 修饰实例方法,作用于当前实例加锁,进入同步代码前要获得当前实例的锁
  • 修饰静态方法,作用于当前类对象加锁,进入同步代码前要获得当前类对象的锁
  • 修饰代码块,并指定加锁对象,对给定对象加锁,进入同步代码库前要获得给定对象的锁

image-20211219144635798

synchronized代码块底层原理

static final Object lock = new Object();
static int counter = 0;
// 定义一个共享变量,让counter进行累加,注意使用的是synchronize同步代码块执行
public static void main(String[] args) {
     synchronized (lock) {
     	counter++;
     }
}

对应的字节码为:(标注为个人见解)

从字节码中可知同步语句块的实现使用的是monitorenter 和 monitorexit 指令

image-20211219112159964

synchronized方法底层原理

static int counter = 0;
// 定义一个共享变量,让counter进行累加,注意使用的是synchronize修饰实例方法
public synchronized void increment(){
    counter++;
}

对应的字节码为:

public synchronized void increment();
    descriptor: ()V
    // 方法标识ACC_PUBLIC代表public修饰,ACC_SYNCHRONIZED指明该方法为同步方法
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=3, locals=1, args_size=1
         0: aload_0
         1: dup
         2: getfield      #2   
         5: iconst_1
         6: iadd
         7: putfield      #2  
        10: return
}

从字节码中可以看出,synchronized 修饰的方法没有使用 monitorenter 指令和 monitorexit 指令,使用ACC_SYNCHRONIZED 标识,指明该方法是一个同步方法,JVM通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。

synchronized 原理进阶

锁的状态总共有四种,无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁,但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级。

偏向锁

偏向锁是Java 6之后加入的新锁,它是一种针对加锁操作的优化手段,在不存在多线程竞争,而且总是由同一线程多次获得锁的情况下,为了减少同一线程获取锁(会涉及到一些CAS操作,耗时)的代价而引入偏向锁。

偏向锁的核心思想是,如果一个线程获得了锁,那么锁就进入偏向模式,此时对象中的 Mark Word 的结构发生改变,对象中的 hashcode 被替换为当前持有偏向锁的线程ID。当这个线程再次请求锁时,无需再做任何同步操作,即获取锁的过程,这样就省去了大量有关锁申请的操作,从而也就提供程序的性能。适用于没有锁竞争的场合,需要注意的是,偏向锁失败后,并不会立即膨胀为重量级锁,而是先升级为轻量级锁。

image-20211219155707018

关于偏向锁的撤销

  1. 调用了对象的 hashCode,但偏向锁的对象 MarkWord 中存储的是线程 id,如果调用 hashCode 会导致偏向锁被撤销。
    • 轻量级锁会在锁记录中记录 hashCode
    • 重量级锁会在 Monitor 中记录 hashCode
  2. 当有其它线程使用偏向锁对象时,会将偏向锁升级为轻量级锁
  3. 调用 wait/notify

轻量级锁

轻量级锁所适应的场景是线程交替执行同步块的场合。轻量级锁的目标是,减少无实际竞争情况下,使用重量级锁产生的性能消耗,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。

轻量级锁是相对于重量级锁而言的。使用轻量级锁时,将对象 Mark Word 中的部分字节通过 CAS 操作更新指向线程栈中的 Lock Record,如果更新成功,则轻量级锁获取成功,记录锁状态为轻量级锁;否则,说明已经有线程获得了轻量级锁,目前发生了锁竞争(不适合继续使用轻量级锁),接下来膨胀为重量级锁。

|------------------------------------------------------- |--------------------|
|                  Mark Word (32 bits)                   |       State        |
|------------------------------------------------------- |--------------------|
| identity_hashcode:25 | age:4 | biased_lock:0 | lock:01 |       Normal       |
|------------------------------------------------------- |--------------------|
|  thread:23 | epoch:2 | age:4 | biased_lock:1 | lock:01 |       Biased       | // 偏向锁
|------------------------------------------------------- |--------------------|
|               ptr_to_lock_record:30          | lock:00 | Lightweight Locked | // 轻量级锁
|------------------------------------------------------- |--------------------|

当退出 synchronized 代码块(解锁时)锁记录的值不为 null,这时使用 cas 将 Mark Word 的值恢复给对象头。如果成功,则解锁成功,如果失败,失败,说明轻量级锁进行了锁膨胀或已经升级为重量级锁,进入重量级锁解锁流程。

锁膨胀

如果在尝试加轻量级锁的过程中,CAS 操作无法成功,这时一种情况就是有其它线程为此对象加上了轻量级锁(有竞争),这时需要进行锁膨胀,将轻量级锁变为重量级锁。

image-20211219162545118

当 Thread-1 进行轻量级加锁时,Thread-0 已经对该对象加了轻量级锁。这时 Thread-1 加轻量级锁失败,进入锁膨胀流程。

image-20211219163215529

自旋优化

自旋锁:是指当一个线程在获取锁的时候,如果锁已经被其它线程获取,那么该线程将循环等待,然后不断的判断锁是否能够被成功获取,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起。自旋锁的目标是降低线程切换的成本。通过自旋锁,可以减少线程阻塞造成的线程切换(包括挂起线程和恢复线程)。

image-20211219215644642
image-20211219215704823

  • 自旋会占用 CPU 时间,单核 CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。

  • 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会 高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。

  • Java 7 之后不能控制是否开启自旋功能

CPU 自旋就是浪费,多核 CPU 自旋才能发挥优势。

  • 在 Java 6 之后自旋锁是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会 高,就多自旋几次;反之,就少自旋甚至不自旋,总之,比较智能。

  • Java 7 之后不能控制是否开启自旋功能

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值