• 对象锁——synchronized关键字


      jdk1.6之后对synchronized进行了优化,为了减少获得锁和释放锁带来的性能消耗,引入了偏向锁和轻量级锁,协调线程安全性和性能的平衡。这种优化主要解决上下文频繁切换,由于Java层面的线程与操作系统的原生线程有映射关系,如果要将一个线程进行阻塞或唤起都需要操作系统的协助,这就需要从用户态切换到内核态来执行,这种切换代价十分昂贵,很耗处理器时间。

               * 可重入锁,主要解决自己锁死自己的问题。

               * 非公平锁,提高执行性能。

               * 悲观锁,不管是否存在竞争,都会加锁。乐观锁:CAS,它涉及到三个操作数:内存值、预期值、新值。当且仅当预期值和内存值相等时才将内存值修改为新值。它具有原子性,由CPU硬件指令实现保证,即使用JNI调用Native方法调用由C++编写的硬件级别指令。

    1、锁的原理

      synchronized也叫对象锁,因为它是通过对对象加锁来实现的。  

    (1)对象内存布局

      对象在内存中的布局可以分为:对象头、实例数据、对齐填充。

         

    (2)对象头中的Mark Word(对象标记)

      Mark Word记录了对象和锁有关的信息,当某个对象被synchronized当成同步锁时,围绕这个锁的一系列操作都和Mark Word有关系。Mark Word在32位虚拟机的长度是32bit,在64位虚拟机的长度是64bit。主要存储着该对象锁的当前状态数据:

    锁状态 25bit 4bit 1bit 2bit
    23bit 2bit 是否偏向锁 锁标志位
    无锁 对象的hashcode 分代年龄 0 01
    偏向锁 线程id Epoch f分代年龄 1 01
    轻量级锁 指向栈中锁记录的指针 00
    重量级锁 指向重量级锁的指针 10
    GC标记 11

      线程在获取锁的时候,实际上就是获得一个监视器对象(monitor),monitor可以认为是一个同步对象,所有的java对象都有一个monitor。多个线程访问同步代码块时,就是去争抢锁对象的监视器去修改对象中的锁标识。

    2、synchronized锁的升级

       锁升级的过程:

        

    (1)偏向锁(适合竞争较少的场景)

      偏向锁的获取:

        当对象锁第一次被获取时,在Mark Word中记录下该线程的ID,这个线程称为偏向线程。

        *  当有线程获取该对象锁时,首先根据对象头判断是否处于可偏向状态(biased_lock=1,且线程ID为空);

        *  如果是可偏向状态,则通过cas操作,尝试把当前线程ID写入到Mark Word。

          ——cas写入成功,表示已经获得了所对象的偏向锁;

          ——cas写入失败,表示其他线程已经获得了偏向锁,存在竞争,需要撤销已获得偏向锁的线程,并把该对象锁升级为轻量级锁(注意:这个操作需要在全局安全点,也就是没有线程在执行字节码的时候才能执行)

        *  如果是已偏向状态,检查Mark Word中存储的线程ID是否等于当前线程ID

          ——相等,则不需再次获得锁,直接执行同步代码块;

          ——不相等,说明当前锁偏向于其他线程,存在竞争,需要撤销已获得偏向锁的线程,并把该对象锁升级为轻量级锁(执行时机同上)。  

      偏向锁的撤销:

        对原持有偏向锁的线程进行撤销时,原偏向线程有两种情况:

        *  偏向线程同步代码块执行完了,判断Epoch是否达到阈值(默认40),达到阈值则升级为轻量级锁,没达到则会把对象头设置为无锁状态,Epoch+1,所有线程可以基于CAS重新获取该偏向锁。

        *  偏向线程同步代码块还没执行完,此时会把原偏向线程持有的偏向锁升级为轻量级锁后,继续执行同步代码块。

      注意:在实际应用开发中,绝大部分情况下一定会存在 2 个以 上的线程竞争,那么如果开启偏向锁,反而会提升获取锁 的资源消耗。所以可以通过jvm参数 UseBiasedLocking 来设置开启或关闭偏向锁

    (2)轻量级锁(适合同步代码块执行时间很短的场景)

      锁升级为轻量级锁之后,锁对象的Mark Word也会进行相应的变化,升级为轻量级锁的过程:

        *  线程在自己的线程栈中创建锁记录Lock Record。

        *  将锁对象的对象头中的Mark Word复制到线程刚刚创建的锁记录中。

        *  将锁记录中的owner指针指向锁对象。

        *  将锁对象的对象头中的Mark Word替换为指向锁记录的指针。

                                          

     自旋锁:

       轻量级锁在加锁过程中用到了自旋锁。所谓自旋,就是当有线程A来竞争锁时,A线程会在原地循环等待,而不是把A线程阻塞(这里阻塞指将线程挂起)。注意:自旋相当于执行一个空的for循环,也是会消耗CPU资源,所以轻量级锁适合同步代码块执行时间很快的场景,这样自旋时间不会太长。jdk1.6之前自旋是有一定次数限制的,默认10次,可以通过preBlockSpin来修改,jdk1.6之后引入了自适应自旋锁。

      自适应自旋锁:自适应意味着自旋的次数不是固定的,而是根据前一次在同一个锁上自旋的时间及锁拥有者的状态来决定。如果在同一个锁对象上,自旋等待刚刚成功获得过锁,那么虚拟机会认为这次自旋也可能成功,并允许这次自旋等待相对更长的时间;如果对于某个锁,自旋很少成功过,那在以后尝试获取该锁时将省略自旋过程,直接阻塞线程,避免浪费CPU资源。

    轻量级锁与自旋锁的不同:

      *  轻量级锁每次在退出同步代码块时都需要释放锁,而偏向锁只有在竞争时才释放锁。

      *  轻量级锁进入和退出同步代码块(加锁和解锁)时都要通过cas操作更新对象头的Mark Word。

      *  线程争夺轻量级锁失败时,会自旋尝试争抢。

    轻量级锁加锁和释放锁的流程:

          

    (3)重量级锁

      对于重量级锁,线程只能被挂起阻塞来等待被唤醒再去重新竞争锁。

      如果升级为重量级锁,synchronized作用在不同位置,底层实现原理不同:

        *  加在代码块上,是通过监视器monitorenter(monitor+1)和monitorexit(monitor-1)控制的。

        *  加在方法上,是通过ACC_SYNCHRONIZED(存储在运行时常量池中的method-info结构中)标志位来控制,如果一个线程进入同步方法,判断该标志位被设置,会尝试获取monitor。

            

      monitor依赖操作系统的MutexLock(互斥锁)来实现,线程被阻塞后便进入内核(Linux)调度状态,这个会导致系统在用户态和内核态之间来回切换,严重影响性能。

    3、wait和notify/notifyAll

            

      

       

  • 相关阅读:
    文件参数Python读取wav格式文件
    电子工程术语和定义列表,按字母顺序排列
    MAC地址加减1算法
    uboot通过kernel command line 动态分区 CONFIG_MTD_CMDLINE_PARTS
    c调用shell脚本
    busybox提示can't access tty.job control turned off
    cut命令如何截取以空格隔开的字段
    DS28E01100
    busybox 中的ntpd使用
    Debugfs
  • 原文地址:https://www.cnblogs.com/jing-yi/p/12761826.html
Copyright © 2020-2023  润新知