一文打通锁升级(偏向锁,轻量级锁,重量级锁)

前置知识:synchronized

 在JavaSE1.6以前,synchronized都被称为重量级锁。但是在JavaSE1.6的时候,对synchronized进行了优化,引入了偏向锁和轻量级锁,以及锁的存储结构和升级过程,减少了获取锁和释放锁的性能消耗,有些情况下它也就不那么重了。

在同步方法中,使用了flag标记ACC_SYNCHRONIZED,当调用方法时,调用指令会检查方法的ACC_SYNCHRONIZED访问标志是否设置。如果设置了,执行线程先持有同步锁,然后执行方法,最后在方法执行完毕释放锁。

同步代码块是使用monitorenter和monitorexit指令进行同步的。monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,并且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象的monitor所有权,即尝试获取对象的锁。

 synchronized锁升级

synchronized锁优化的背景:
用锁能实现数据的安全性,但是会带来性能下降。
无锁能够基于线程并行提升程序性能,但是会带来线程安全性下降。synchronized锁:根据对象头的mark word 锁标志位来确定当前属于哪一种锁。

 为什么会存在锁升级现象?

在java5及其以前,只有synchronized,这个是重量级锁,是操作系统级别的重量级操作。假如锁的竞争比较激烈,性能下降。因为存在用户态和内核态之间的转换。

java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在户态与核心态之间切换,这种切换会消耗大量的系统资源,因为用户态与内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。

发布Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(montor)是依赖于底层的操作系统的MutexLoCK(系统工斥量)来实现的,挂起线程和恢复线程都需要转入内核态去完成,阻塞或唤醒一个Java线程需要换作系统切换CPU状态来完成,这种状态切换需要耗费处理器时间,如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长”,时间成本相对较高。这也是为什么早期的synchronized效率低的原因Java 6之后,为了减少获得锁和释放锁所带来的性能消耗,引入了轻量级锁和偏向锁

偏向锁

顾名思义,它会偏向于第一个访问锁的线程

当一段同步代码一直被同一个线程多次访问,由于只有一个线程那么该线程在后续访问时便会自动获得锁,在多线程的情况下,锁不仅不存在多线程竞争,还存在锁由同一个线程多次获取,偏向锁解释在这种情况下出现的,她的出现是未来解决只有在一个线程执行同步时提高性能

  • 如果在运行过程中,同步锁只有一个线程访问,不存在多线程争用的情况,则线程是不需要触发同步的,这种情况下,就会给线程加一个偏向锁。线程第二次到达同步代码块时,会判断此时持有锁的线程是否就是自己,如果是则正常往下执行。由于之前没有释放锁,这里也就不需要重新加锁。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。(避免了线程的上下切换),也即偏向锁在资源没有竞争情况下消除了同步语句,懒的连CAS操作都不做了,直接提高程序性能。
  • 如果在运行过程中,遇到了其他线程抢占锁,则持有偏向锁的线程会被挂起,JVM会消除它身上的偏向锁,将锁恢复到标准的轻量级锁。偏向锁通过消除资源无竞争情况下的同步原语,进一步提高了程序的运行性能。一旦有第二个线程加入锁竞争,偏向锁就升级为轻量级锁(自旋锁)。升级为轻量级锁的时候需要撤销偏向锁,撤销偏向锁的时候会导致STW(stop the word)操作;

 

实际上偏向锁在JDK1.6之后是默认开启的,但是启动时间有延迟(4秒),
所以需要添加参数-XX:BiasedLockingStartupDelay=0,让其在程序启动时立刻启动 

开启偏向锁:

-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0

关闭偏向锁:关闭之后程序默认会直接进入------->轻量级锁状态

-XX:-UseBiasedLocking

 

轻量级锁

多线程竞争,但是任意时刻最多只有一个线程竞争,即不存在锁竞争太过激烈的情况,也就没有线程阻塞。

有线程来参与锁的竞争,但是获取锁的冲突时间极短。本质就是自旋锁CAS

主要目的: 在没有多线程竞争的前提下,通过CAS减少重量级锁使用操作系统互斥量产生的性能消耗,说白了先自旋,不行才升级阻塞。

升级时机:当关闭偏向锁功能或多线程竞争偏向锁会导致偏向锁升级为轻量级锁

假如线程A已经拿到锁,这时线程B又来抢该对象的锁,由于该对象的锁已经被线程A拿到,当前该锁已是偏向锁了。而线程B在争抢时发现对象头Mark Word中的线程ID不是线程B自己的线程ID(而是线程A),那线程B就会进行CAS操作希望能获得锁。

此时线程B操作中有两种情况:

如果锁获取成功,直接替换Mark Word中的线程ID为B自己的ID(A→B),重新偏向于其他线程(即将偏向锁交给其他线程,相当于当前线程“被“释放了锁),该锁会保持偏向锁状态,A线程Over,B线程上位;

如果锁获取失败,则偏向锁升级为轻量级锁(设置偏向锁标识为0并设置锁标志位为00),此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程B会进入自旋等待获得该轻量级锁。

Java6之前

默认启用,默认情况下自旋的次数是10次,或者自旋线程数超过cpu核数一半。

Java6之后

变为自适应自旋锁。意味着自旋的次数不是固定不变的,而是根据:拥有锁线程的状态来决定,或者同一个锁上一次自旋的时间。

线程如果自旋成功了,那下次自旋的最大次数会增加,因为JVM认为既然上次成功了,那么这一次也很大概率会成功。反之,如果很少会自旋成功,那么下次会减少自旋的次数甚至不自旋,避免CPU空转。

重量级锁

适用于:有大量的线程参与锁的竞争,冲突性很高。

重量级锁原理

Java中synchronized的重量级锁,是基于进入和退出Monitor对象实现的。在编译时会将同步块的开始位置插入monitor enter指令,在结束位置插入monitor exit指令。

当线程执行到monitor enter指令时,会尝试获取对象所对应的Monitor所有权,如果获取到了,即获取到了锁,会在Monitor的owner中存放当前线程的id,这样它将处于锁定状态,除非退出同步块,否则其他线程无法获取到这个Monitor。

猜你喜欢

转载自blog.csdn.net/m0_62436868/article/details/129909296