对象锁——synchronized关键字

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

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

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

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

1、锁的原理

  synchronized也叫对象锁(类级别则是Class对象),因为它是通过对对象加锁来实现的。  

(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

        

 

一般禁用偏向锁,以及偏向锁的撤销可以参考这篇文章:https://blog.csdn.net/qq_42046105/article/details/122374767?spm=1001.2014.3001.5501

 

posted @ 2020-04-23 16:37  jingyi_up  阅读(209)  评论(0编辑  收藏  举报