Monitor原理

Monitor

  • java对象头

    • 普通对象【32bit的jvm】:
      • Mark word【32 bits】:对象信息;
      • Klass Word【32 bits】:指向Klass对象【Class对象】;
    • 数组对象:
      • Mark word【32 bits】
      • Klass Word【32bits】
      • arraylength【32bits】
    • Mark word的结构
      • normal state:
        • hashcode【25bits】+age【3bits,年代区别】+biased_lock【1bit】+ 锁标志【2bits】;
      • Biased state:
        • thread【23bits】+epoch【2】+biased_lock【1bit】+ 锁标志【2bits】;
      • Lightweit Locked state:
        • ptr_to_lock_record【30bits】+ 锁标志【2bits】;
      • heavyweight Locked state:
        • ptr_to_heavyweight_monitor【30bits】+ 锁标志【2bits】
      • marked for GC
        • 锁标志【2bits】;
  • Monitor【操作系统提供】工作原理:

    • 当使用synchronized时,锁的对象会关联一个Monitor对象;
    • monitor对象:
      • Owner:只能有一个对象;
      • EntryList:阻塞【Blocked】的链表,当锁释放的时候,里面的线程会被唤醒;
      • WaitSet:
    • 字节码流程:
      • 遇到synchronized,将lock对象的Mark word置换位Monitor引用,同时将mark word存储到Monitor中;
      • 而且底层有:try-catch-finally机制;
  • synchronized的优化:

    • 轻量级锁:多个线程错开访问,如果有竞争就会升级;
    • synchronized优先轻量级锁;
      • 线程内部创建锁记录对象
        • 锁记录对象【LockRecord】:
          • 【lock record地址,00【轻量级锁标志】】
          • 【Object referrence】;
        • 将Object referrence指向锁对象,然后使用CAS【乐观锁】的方式替换Object中的mark word的值和【lock record地址,00】
      • 如果成功了,则表示该线程给对象加锁成功;
      • 如果失败了:
        • 如果有其他线程已经持有了该对象的轻量级锁,则有竞争,会进入锁膨胀过程;
        • 如果是自己持有,那么再添加条Lock record【mark word 00:为null】作为重入的技术;
      • 当退出的时候,如果取值有null,则表示有重入,pop出就行;
      • 锁记录不为null时,使用CAS将mark word进行恢复;
        • 成功:解锁成功;
        • 失败:进入重量级解锁流程;
    • 锁膨胀:
      • 另一个线程加轻量级锁失败后,进入锁膨胀:
      • 会给锁对象申请一个monitor锁,并将让Object的mark word指向monitor地址;
      • 然乎自己进入monitor的EntryList进行Blocked;
      • 当持有轻量级锁退出来后,发现使用CAS给锁对象恢复mark word的时候,会失败;
        • 则会根据锁对象的Monitor找到monitor对象,将Ower设置为null;并唤醒EntryList中的线程;
    • 自旋优化:
      • 进入阻塞会发生上下文切换,降低性能【多核cpu才有意义】;
      • 自旋循环几次后重试获取锁【具体自旋次数,比较智能】;
    • 偏向锁:
      • 轻量级锁在发生锁重入的时候,仍然会要执行CAS操作【进行比较,替换】;
      • 在第一次执行CAS操作的时候,将线程ID【操作系统的线程ID】设置到锁对象的的mark word头里面,之后发现线程ID是自己,表示没竞争,就不用重新执行CAS操作;
      • 一个对象创建时:
        • 如果开启了偏向锁【默认开启】,那么对象创建后,mark word的最后三位是101,这时候他的thread,epoch,age都是0;
        • 偏向锁默认是延迟的,不会再程序启动后立即生效;
        • 如果没有开启偏向锁,那么mark word的最后三位是001,他的hashcode,age都是0,只有第一次用到时候,才会赋值;
        • 当开启偏向锁,主动使用synchronized的偏向锁后,线程ID的值不会消失,会被持续属于这个对象【仅仅第一次的时候】;
        • 偏向锁撤销【禁用】:
          • 当开启偏向锁后,在调用hashcode,会直接将该对象的偏向锁撤销【因为没位置存hashcode的值了】。
          • 当有其他线程再次使用该偏向锁时,会直接将该对象的偏向锁撤销,然后升级为轻量级锁,之后成正常状态;
          • 调用wait/notify时,因为这个只有重量级锁才会有,所以会直接升级为重量级锁;
      • 批量重偏向【jvm的优化】:
        • 如果对下给你被多个线程访问,但是没有竞争,这时偏向线程1的对象仍然有机会重新偏向线程2;
        • 【批量重偏向】:当批量偏向线程1的对象给线程2做锁的时候,会产生偏向锁撤销,当阈值超过20次的时候,jvm会自动优化之后的对象重新偏向至线程2【偏向锁撤销是耗费效率的】;
        • 【批量撤销】:当撤销偏向阈值超过40次过后,jvm会认为自己有问题,就不应该偏向,所以整个类的对象都会变为不可偏向,包括新建对象【第40个】;
    • 锁消除:
      • 当代码被反复执行,JIT就会热点代码优化,当并对象并不存在多线程的共享,所以就会把锁消除掉,以提升性能;
  • wait / notify原理:

    • 当Owner线程调用wait方法的时候,即可进入monitor的WaitSet集合,变成waiting状态;

    • Monitor的EntryList会在Owner释放锁的时候被唤醒;

    • 但是WaitSet则是通过notify 或者 notifyAll方法时,才会被唤醒,并且进入EnttryList进行重新排队;

    • api方法:前提获取了Monitor的锁后,才能调用

      • obj.wait():让进入到监视器的线程到waitSet等待;

      • obj.notify():在object上正在waitSet等待的线程中挑一个唤醒;

      • obj.notifyAll:在object正在waitSet等待的所有线程唤醒;

        它们都是Object对象的方法;

    • sleep(long n)和wait(long n)的区别:

      • sleep是一个Thread的静态方法,wait是一个Object方法;
      • wait需要配合synchronized使用;
      • sleep在休眠的时候,不会释放锁,wait在等待的时候,会释放锁;
      • 这两个都是带时限的等待,属于TIMED_WAITING状态;
      • sleep占用cpu效率更低;
  • park和unpark(Thread n)方法:

    • wait,notifyAll必须配合Monitor使用,而park,unpark不必;
    • park和unpark是以线程为单位阻塞和唤醒,notify是随机唤醒,notifyAll是唤醒所有;
    • park和unpark,可以先使用unpark,而wait和notify不能,多次调用unpark只是有效一次;
    • 每个线程都有一个Paker对象,C代码编写,
    • t.join():则当前线程进入waiting状态,当前线程在t1线程对象的监视器上等待;
    • 可以调用interrupt()方法打断等待的:
      • park();
      • sleep();
  • 死锁:jconsole工具;

  • 活锁:出现两个线程相互改变对象的结束条件,导致谁也无法结束;

  • 饥饿:由于线程的优先级太低,得不到cup调度,也不能够结束;

posted @ 2025-03-23 17:21  烟雨断桥  阅读(76)  评论(0)    收藏  举报