美文网首页
Java关键字之synconized

Java关键字之synconized

作者: 小丸子的呆地 | 来源:发表于2021-07-11 08:42 被阅读0次

    内容大部分摘自马士兵教育

    JDK早期,synchronized 叫做重量级锁, 因为申请锁资源必须通过kernel, 系统调用

    markword

    查看工具:JOL = Java Object Layout
        <dependencies>
            <!-- https://mvnrepository.com/artifact/org.openjdk.jol/jol-core -->
            <dependency>
                <groupId>org.openjdk.jol</groupId>
                <artifactId>jol-core</artifactId>
                <version>0.9</version>
            </dependency>
        </dependencies>
    

    jdk8u: markOop.hpp

    // Bit-format of an object header (most significant first, big endian layout below):
    //
    //  32 bits:
    //  --------
    //             hash:25 ------------>| age:4    biased_lock:1 lock:2 (normal object)
    //             JavaThread*:23 epoch:2 age:4    biased_lock:1 lock:2 (biased object)
    //             size:32 ------------------------------------------>| (CMS free block)
    //             PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
    //
    //  64 bits:
    //  --------
    //  unused:25 hash:31 -->| unused:1   age:4    biased_lock:1 lock:2 (normal object)
    //  JavaThread*:54 epoch:2 unused:1   age:4    biased_lock:1 lock:2 (biased object)
    //  PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
    //  size:64 ----------------------------------------------------->| (CMS free block)
    //
    //  unused:25 hash:31 -->| cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && normal object)
    //  JavaThread*:54 epoch:2 cms_free:1 age:4    biased_lock:1 lock:2 (COOPs && biased object)
    //  narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
    //  unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
    

    synchronized的横切面详解

    1. synchronized原理
    2. 升级过程
    3. 汇编实现
    4. vs reentrantLock的区别

    synchronized原理

    java源码层级

    synchronized(o)

    字节码层级

    monitorenter moniterexit

    JVM层级(Hotspot)

    package com.mashibing.insidesync;
    
    import org.openjdk.jol.info.ClassLayout;
    
    public class T01_Sync1 {
      
    
        public static void main(String[] args) {
            Object o = new Object();
    
            System.out.println(ClassLayout.parseInstance(o).toPrintable());
        }
    }
    
    com.mashibing.insidesync.T01_Sync1$Lock object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4   (object header)  05 00 00 00 (00000101 00000000 00000000 00000000) (5)
          4     4   (object header)  00 00 00 00 (00000000 00000000 00000000 00000000) (0)
          8     4   (object header)  49 ce 00 20 (01001001 11001110 00000000 00100000) (536923721)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes total
    
    com.mashibing.insidesync.T02_Sync2$Lock object internals:
     OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
          0     4   (object header)  05 90 2e 1e (00000101 10010000 00101110 00011110) (506368005)
          4     4   (object header)  1b 02 00 00 (00011011 00000010 00000000 00000000) (539)
          8     4   (object header)  49 ce 00 20 (01001001 11001110 00000000 00100000) (536923721)
         12     4        (loss due to the next object alignment)
    Instance size: 16 bytes
    Space losses: 0 bytes internal + 4 bytes external = 4 bytes tota
    

    InterpreterRuntime:: monitorenter方法

    IRT_ENTRY_NO_ASYNC(void, InterpreterRuntime::monitorenter(JavaThread* thread, BasicObjectLock* elem))
    #ifdef ASSERT
      thread->last_frame().interpreter_frame_verify_monitor(elem);
    #endif
      if (PrintBiasedLockingStatistics) {
        Atomic::inc(BiasedLocking::slow_path_entry_count_addr());
      }
      Handle h_obj(thread, elem->obj());
      assert(Universe::heap()->is_in_reserved_or_null(h_obj()),
             "must be NULL or an object");
      if (UseBiasedLocking) {
        // Retry fast entry if bias is revoked to avoid unnecessary inflation
        ObjectSynchronizer::fast_enter(h_obj, elem->lock(), true, CHECK);
      } else {
        ObjectSynchronizer::slow_enter(h_obj, elem->lock(), CHECK);
      }
      assert(Universe::heap()->is_in_reserved_or_null(elem->obj()),
             "must be NULL or an object");
    #ifdef ASSERT
      thread->last_frame().interpreter_frame_verify_monitor(elem);
    #endif
    IRT_END
    

    synchronizer.cpp

    revoke_and_rebias

    void ObjectSynchronizer::fast_enter(Handle obj, BasicLock* lock, bool attempt_rebias, TRAPS) {
     if (UseBiasedLocking) {
        if (!SafepointSynchronize::is_at_safepoint()) {
          BiasedLocking::Condition cond = BiasedLocking::revoke_and_rebias(obj, attempt_rebias, THREAD);
          if (cond == BiasedLocking::BIAS_REVOKED_AND_REBIASED) {
            return;
          }
        } else {
          assert(!attempt_rebias, "can not rebias toward VM thread");
          BiasedLocking::revoke_at_safepoint(obj);
        }
        assert(!obj->mark()->has_bias_pattern(), "biases should be revoked by now");
     }
    
     slow_enter (obj, lock, THREAD) ;
    }
    
    void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) {
      markOop mark = obj->mark();
      assert(!mark->has_bias_pattern(), "should not see bias pattern here");
    
      if (mark->is_neutral()) {
        // Anticipate successful CAS -- the ST of the displaced mark must
        // be visible <= the ST performed by the CAS.
        lock->set_displaced_header(mark);
        if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) {
          TEVENT (slow_enter: release stacklock) ;
          return ;
        }
        // Fall through to inflate() ...
      } else
      if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) {
        assert(lock != mark->locker(), "must not re-lock the same lock");
        assert(lock != (BasicLock*)obj->mark(), "don't relock with same BasicLock");
        lock->set_displaced_header(NULL);
        return;
      }
    
    #if 0
      // The following optimization isn't particularly useful.
      if (mark->has_monitor() && mark->monitor()->is_entered(THREAD)) {
        lock->set_displaced_header (NULL) ;
        return ;
      }
    #endif
    
      // The object header will never be displaced to this lock,
      // so it does not matter what the value is, except that it
      // must be non-zero to avoid looking like a re-entrant lock,
      // and must not look locked either.
      lock->set_displaced_header(markOopDesc::unused_mark());
      ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD);
    }
    

    inflate方法:膨胀为重量级锁

    锁升级

    锁升级过程

    JDK8 markword实现表:
    lock_step.png markword-64.png

    自旋锁什么时候升级为重量级锁?

    在自旋一定次数之后,仍未拿到锁。

    为什么有自旋锁还需要重量级锁?

    自旋是消耗CPU资源的,如果锁的时间长,或者自旋线程多,CPU会被大量消耗

    重量级锁有等待队列,所有拿不到锁的进入等待队列,不需要消耗CPU资源

    偏向锁是否一定比自旋锁效率高?

    不一定,在明确知道会有多线程竞争的情况下,偏向锁肯定会涉及锁撤销,这时候直接使用自旋锁

    JVM启动过程,会有很多线程竞争(明确),所以默认情况启动时不打开偏向锁,过一段儿时间再打开

    new - 偏向锁 - 轻量级锁 (无锁, 自旋锁,自适应自旋)- 重量级锁

    synchronized优化的过程和markword息息相关

    用markword中最低的三位代表锁状态 其中1位是偏向锁位 两位是普通锁位

    1. Object o = new Object()
      锁 = 0 01 无锁态
      注意:如果偏向锁打开,默认是匿名偏向状态

    2. o.hashCode()
      001 + hashcode

      00000001 10101101 00110100 00110110
      01011001 00000000 00000000 00000000
      

      little endian big endian

      00000000 00000000 00000000 01011001 00110110 00110100 10101101 00000000

    3. 默认synchronized(o)
      00 -> 轻量级锁
      默认情况 偏向锁有个时延,默认是4秒
      why? 因为JVM虚拟机自己有一些默认启动的线程,里面有好多sync代码,这些sync代码启动时就知道肯定会有竞争,如果使用偏向锁,就会造成偏向锁不断的进行锁撤销和锁升级的操作,效率较低。

      -XX:BiasedLockingStartupDelay=0
      
    4. 如果设定上述参数
      new Object () - > 101 偏向锁 ->线程ID为0 -> Anonymous BiasedLock
      打开偏向锁,new出来的对象,默认就是一个可偏向匿名对象101

    5. 如果有线程上锁
      上偏向锁,指的就是,把markword的线程ID改为自己线程ID的过程
      偏向锁不可重偏向 批量偏向 批量撤销

    6. 如果有线程竞争
      撤销偏向锁,升级轻量级锁
      线程在自己的线程栈生成LockRecord ,用CAS操作将markword设置为指向自己这个线程的LR的指针,设置成功者得到锁

    7. 如果竞争加剧
      竞争加剧:有线程超过10次自旋, -XX:PreBlockSpin, 或者自旋线程数超过CPU核数的一半, 1.6之后,加入自适应自旋 Adapative Self Spinning , JVM自己控制
      升级重量级锁:-> 向操作系统申请资源,linux mutex , CPU从3级-0级系统调用,线程挂起,进入等待队列,等待操作系统的调度,然后再映射回用户空间

    (以上实验环境是JDK11,打开就是偏向锁,而JDK8默认对象头是无锁)

    偏向锁默认是打开的,但是有一个时延,如果要观察到偏向锁,应该设定参数

    如果计算过对象的hashCode,则对象无法进入偏向状态!

    轻量级锁重量级锁的hashCode存在与什么地方?

    答案:线程栈中,轻量级锁的LR中,或是代表重量级锁的ObjectMonitor的成员中

    关于epoch: (不重要)

    批量重偏向与批量撤销渊源:从偏向锁的加锁解锁过程中可看出,当只有一个线程反复进入同步块时,偏向锁带来的性能开销基本可以忽略,但是当有其他线程尝试获得锁时,就需要等到safe point时,再将偏向锁撤销为无锁状态或升级为轻量级,会消耗一定的性能,所以在多线程竞争频繁的情况下,偏向锁不仅不能提高性能,还会导致性能下降。于是,就有了批量重偏向与批量撤销的机制。

    原理以class为单位,为每个class维护解决场景批量重偏向(bulk rebias)机制是为了解决:一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,这样会导致大量的偏向锁撤销操作。批量撤销(bulk revoke)机制是为了解决:在明显多线程竞争剧烈的场景下使用偏向锁是不合适的。

    一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。每个class对象会有一个对应的epoch字段,每个处于偏向锁状态对象的Mark Word中也有该字段,其初始值为创建该对象时class中的epoch的值。每次发生批量重偏向时,就将该值+1,同时遍历JVM中所有线程的栈,找到该class所有正处于加锁状态的偏向锁,将其epoch字段改为新值。下次获得锁时,发现当前对象的epoch值和class的epoch不相等,那就算当前已经偏向了其他线程,也不会执行撤销操作,而是直接通过CAS操作将其Mark Word的Thread Id 改成当前线程Id。当达到重偏向阈值后,假设该class计数器继续增长,当其达到批量撤销的阈值后(默认40),JVM就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后,对于该class的锁,直接走轻量级锁的逻辑。

    加锁,指的是锁定对象

    锁升级的过程
    JDK较早的版本 OS的资源 互斥量 用户态 -> 内核态的转换 重量级 效率比较低

    现代版本进行了优化
    无锁 - 偏向锁 -轻量级锁(自旋锁)-重量级锁

    偏向锁 - markword 上记录当前线程指针,下次同一个线程加锁的时候,不需要争用,只需要判断线程指针是否同一个,所以,偏向锁,偏向加锁的第一个线程 。hashCode备份在线程栈上 线程销毁,锁降级为无锁

    有争用 - 锁升级为轻量级锁 - 每个线程有自己的LockRecord在自己的线程栈上,用CAS去争用markword的LR的指针,指针指向哪个线程的LR,哪个线程就拥有锁

    自旋超过10次,升级为重量级锁 - 如果太多线程自旋 CPU消耗过大,不如升级为重量级锁,进入等待队列(不消耗CPU)-XX:PreBlockSpin

    自旋锁在 JDK1.4.2 中引入,使用 -XX:+UseSpinning 来开启。JDK 6 中变为默认开启,并且引入了自适应的自旋锁(适应性自旋锁)。

    自适应自旋锁意味着自旋的时间(次数)不再固定,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也是很有可能再次成功,进而它将允许自旋等待持续相对更长的时间。如果对于某个锁,自旋很少成功获得过,那在以后尝试获取这个锁时将可能省略掉自旋过程,直接阻塞线程,避免浪费处理器资源。

    偏向锁由于有锁撤销的过程revoke,会消耗系统资源,所以,在锁争用特别激烈的时候,用偏向锁未必效率高。还不如直接使用轻量级锁。

    锁重入

    sychronized是可重入锁

    重入次数必须记录,因为要解锁几次必须得对应

    偏向锁 自旋锁 -> 线程栈 -> LR + 1

    重量级锁 -> ? ObjectMonitor字段上

    synchronized最底层实现
    
    public class T {
        static volatile int i = 0;
        
        public static void n() { i++; }
        
        public static synchronized void m() {}
        
        publics static void main(String[] args) {
            for(int j=0; j<1000_000; j++) {
                m();
                n();
            }
        }
    }
    
    

    java -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly T

    C1 Compile Level 1 (一级优化)

    C2 Compile Level 2 (二级优化)

    找到m() n()方法的汇编码,会看到 lock comxchg .....指令

    synchronized vs Lock (CAS)

     在高争用 高耗时的环境下synchronized效率更高
     在低争用 低耗时的环境下CAS效率更高
     synchronized到重量级之后是等待队列(不消耗CPU)
     CAS(等待期间消耗CPU)
     
     一切以实测为准
    

    锁消除 lock eliminate

    public void add(String str1,String str2){
             StringBuffer sb = new StringBuffer();
             sb.append(str1).append(str2);
    }
    

    我们都知道 StringBuffer 是线程安全的,因为它的关键方法都是被 synchronized 修饰过的,但我们看上面这段代码,我们会发现,sb 这个引用只会在 add 方法中使用,不可能被其它线程引用(因为是局部变量,栈私有),因此 sb 是不可能共享的资源,JVM 会自动消除 StringBuffer 对象内部的锁。

    锁粗化 lock coarsening

    public String test(String str){
           
           int i = 0;
           StringBuffer sb = new StringBuffer():
           while(i < 100){
               sb.append(str);
               i++;
           }
           return sb.toString():
    }
    

    JVM 会检测到这样一连串的操作都对同一个对象加锁(while 循环内 100 次执行 append,没有锁粗化的就要进行 100 次加锁/解锁),此时 JVM 就会将加锁的范围粗化到这一连串的操作的外部(比如 while 虚幻体外),使得这一连串操作只需要加一次锁即可。

    锁降级(不重要)

    https://www.zhihu.com/question/63859501

    其实,只被VMThread访问,降级也就没啥意义了。所以可以简单认为锁降级不存在!

    超线程

    一个ALU + 两组Registers + PC

    参考资料

    http://openjdk.java.net/groups/hotspot/docs/HotSpotGlossary.html

    相关文章

      网友评论

          本文标题:Java关键字之synconized

          本文链接:https://www.haomeiwen.com/subject/zlcrpltx.html