JUC-synchronized锁升级

synchronized锁升级

无锁 → 偏向锁 → 轻量级锁 → 重量级锁

无锁:(锁标志位001)


偏向锁:当一段同步代码一直被同一个线程多次访问,由于只有一个线程那么该线程在后续访问时便会自动获得锁 (锁标志位101)

多线程的情况下,锁不仅不存在多线程的竞争,还存在锁由同一线程多次获得的情况

偏向锁就是在这种情况下出现的,它的出现是为了解决只有在一个线程执行同步时提高性能

锁的代码块时,不需要再次加锁和释放锁。而是直接比较对象头里面是否存储了指向当前线程的偏向锁。
如果相等表示偏向锁是偏向于当前线程的,就不需要再尝试获得锁了,直到竞争发生才释放锁。以后每次同步,检查锁的偏向线程ID与当前线程ID是否一致,如果一致直接进入同步。无需每次加锁解锁都去CAS更新对象头。如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。
假如不一致意味着发生了竞争,锁已经不是总是偏向于同一个线程了,这时候可能需要升级变为轻量级锁,才能保证线程间公平竞争锁。偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放偏向锁的。
技术实现:
一个synchronized方法被一个线程抢到了锁时,那这个方法所在的对象就会在它的MarkWord中将偏向锁位修改位1,同时还会有占用前54位来存储线程指针作为标识。若该线程再次访问同一个synchronized方法时,该线程只需去对象头的MarkWord中去判断一下是否有偏向锁指向本身的ID,无需再进入Monitor去竞争对象了。

细化案例Account对象举例说明

偏向锁的操作不用直接桶到操作系统,不涉及用户态到内核态转换,不必要直接升级为最高级,我们以一个account对象的“对象头”为例

假如有一个线程执行到synchronized代码块的时候,JVM使用CAS操作把线程指针ID记录到MarkWord当中,并修改偏向锁标识,表示当前线程获得该锁。锁对象变成偏向锁(通过CAS修改对象头里的锁标志位),字面意思是“偏向于第一个获得它的线程”的锁执行完同步代码块后,线
程并不会主动释放偏向锁。

这时线程获得了锁,可以执行同步代码块。当该线程第二次到达同步代码块时会判断此时持有锁的线程是否还是自己(持有锁的线程ID也在对象头里),JVM通过account对象的MarkWord判断:当前线程ID还在,说明还持有着这个对象的锁,就可以继续进入临界区工作。由于之前没有释放锁
这里也就不需要重新加锁如果自始至终使用锁的线程只有一个,很明显偏向锁几乎没有额外开销,性能极高。

结论;JVM不用和操作系统协商设置Mutex(争取内核),它只需要记录下线程ID就标示自己获得了当前锁,不用操作系统接入
上述就是偏向锁:在没有其他线程竞争的时候,一直偏向偏心当前线程,当前线程可以一直执行。


实际上偏向锁在JDK1.6之后是默认开启的,但是启动时间有延迟,
所以需要添加参数-XX:BiasedLockingStartupDelay=0,让其在程序启动时立刻启动
开启偏向锁
-XX:+UseBiasedlocking -XX:BiasedlockingStartupDelay=0
关闭偏向锁:关闭之后程序默认会直接进入轻量级锁状态。(锁优化的一种方式)
-Xx:-UseBiasedlocking

偏向锁的撤销

偏向锁使用一种等到竞争出现才释放锁的机制,只有当其他线程竞争锁时,持有偏向锁的原来线程才会被撤销。

撤销需要等待全局安全点(STW,该时间点上没有字节码正在执行),同时检查持有偏向锁的线程是否还在执行:

线程正在执行synchronized方法(处于同步块),它还没有执行完,其它线程来抢夺,该偏向锁会被取消掉并出现锁升级为轻量级锁。此时轻量级锁由原持有偏向锁的线程持有,继续执行其同步代码,而正在竞争的线程会进入自旋等待获得该轻量级锁。

线程执行完成synchronized方法(退出同步块),则将对象头设置成无锁状态并撤销偏向锁,重新偏向。


轻量级锁 (锁标志位000)

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

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

偏向锁 1个线程

轻量锁 2个线程


 重量级锁 (锁标志位010)

当自旋达到一定次数仍然没有成功时,升级为重量级锁

自旋次数:在java6之前是10次,或者CPU核数的一半

                  在java6之后是自适应的,自旋的次数不是固定的,而是依据同一个锁上一次自旋的时间和拥有锁线程的状态来决定

有大量的线程参与锁的竞争,冲突性很高


 轻量级锁和偏向锁的区别

1、轻量级锁竞争锁失败时,会自旋尝试抢占锁

2、轻量级锁每次退出同步块需要释放锁,而偏向锁只有在发生竞争锁时才释放锁,性能比轻量级锁高


总结

锁的优缺点对比

synchronized锁升级过程总结:一句话,就是先自旋,不行再阻塞。
实际上是把之前的悲观锁(重量级锁)变成在一定条件下使用偏向锁以及使用轻量级(自旋锁CAS)的形式
synchronized在修饰方法和代码块在字节码上实现方式有很大差异,但是内部实现还是基于对象头的Markword来实现的。
JDK1.6之前synchronized使用的是重量级锁,JDK1.6之后进行了优化,拥有了无锁->偏向锁->轻量级锁->重量级锁的升级过程,而不什么情况都使用重量级锁。

偏向锁:适用于单线程适用的情况,在不存在锁竞争的时候进入同步方法/代码块则使用偏向锁。
轻量级锁:适用于竞争较不激烈的情况(这和乐观锁的使用范围类似),存在竞争时偏向锁升级为轻量级锁,轻量级锁采用的是自旋锁,如果同步方法/代码块执行时间很短的话,采用轻量级锁虽然会占用cpu资源但是相对比使用重量级锁还是更高效。
重量级锁:适用于竞争激烈的情况,如果同步方法/代码块执行时间很长,那么使用轻量级锁自旋带来的性能消耗就比使用重量级锁更严重,这时候就需要升级为重量级锁。


JIT - Just in Time Compiler 即时编译器

锁消除

 锁粗化

/**
 * 锁粗化
 * 假入方法中首尾相接,前后相邻的都是同一个锁对象,JIT编译器会把这几个synchronized合并成一个大块
 * 加粗加大范围,一次申请锁使用即可,避免多次申请和释放锁,提示了性能
 */
public class LockBigDemo {
    static Object objectLock = new Object();

    public static void main(String[] args) {
        new Thread(()->{
            synchronized (objectLock) {
                System.out.println("1");
//            }
//            synchronized (objectLock) {
                System.out.println("2");
//            }
//            synchronized (objectLock) {
                System.out.println("3");
            }
        }, "t1").start();
    }
}

猜你喜欢

转载自blog.csdn.net/qq_39940205/article/details/120773895
今日推荐