Synchronized锁升级(偏向锁、轻量锁、重量锁)

高并发时,同步调用应该考虑到锁的性能消耗。能用无锁的数据结构,就不要用锁;能锁区块,就不要锁整个方法体;能使用对象锁,就不用类锁。

锁的升级:

无锁 => 偏向锁 => 轻量级锁 => 重量级锁

偏向锁:当一段同步代码一直被同一个线程多次访问,那么该线程在后续的访问时会自动获取锁 。

偏向锁:

  1. 当同步代码首次被一个线程访问,那么就会在Mark Word记录该线程的ID,从无锁状态(001)变成偏向锁(101),当同步代码执行结束,该线程并不会释放锁。

  1. 当下一次同步代码被访问时,那么就会检测该线程ID与锁的Mark Word 中的线程ID是否是相同。

  • 相同:则直接进入同步代码,因为之前没有释放锁

  • 不同:表示发生了竞争,会尝试使用CAS来替换Mark Word里面的线程ID。竞争成功则会替换Mark Word 里面的线程ID,竞争失败可能会变成轻量级锁

偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程是不会主动释放锁的。

偏向锁默认在程序启动4秒后才会开启,可以用下面参数更改时间

开启偏向锁(jdk6之后默认开启):-XX:+UseBiasedLocking

将偏向锁延迟时间由4000ms改为0:-XX:BiasedLockingStartupDelay=0

偏向锁的撤销:

撤销需要等待全局安全点,同时检查持有偏向锁的线程是否还在执行。

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

  1. 第一个线程执行完synchronized方法(退出同部分代码块),则将对象头设置成无锁状态并撤销偏向锁,重新偏向。

由于维护成本高,偏向锁在jdk15及之后被移除了。

轻量锁

轻量级锁是为了在线程近乎交替执行同步块时提高性能

在没有多线程的前提下,通过CAS减少重量级锁使用操作系统互斥产生的性能损耗。

轻量锁的加锁:JVM会为每个线程创建用于存储锁的记录空间,官方成为 Displaced Mark Word。若线程获得锁时发现是轻量锁,则会把锁的 Mark Word 复制到Displaced Mark Word 里面。

轻量锁的释放:当前线程会使用CAS操作将Displaced Mark Word里面的内容复制回Mark Word里面。

自旋抢占轻量锁,当自旋到达一定次数依然没有成功获取锁将升级为重量锁

  • jdk6之前:

  1. 默认10次自旋未成功升级锁 使用VM参数修改 -XX:PreBlockSpin=10

  1. 或者自旋线程超过了cpu核心数一半

  • jdk6之后:自适应自旋锁。JVM底层优化,若线程上次自旋成功获取锁,那么下一次就会增加自旋的次数,以保证更大的可能性获取锁;若线程很少自旋成功获取锁,那么下次就会减少自旋的次数,避免cpu空转。

轻量锁与偏向锁的区别:

  1. 争夺轻量锁失败,自旋锁会尝试枪锁。

  1. 轻量锁每次执行完同步代码块都会释放锁,而偏向锁只有在发生竞争时才会释放锁。

重量锁

ObjectMonitor类 操作系统管程monitor

锁升级后hashcode去哪儿了?

偏向锁:Mark Word 存储的是偏向的线程ID

轻量锁:Mark Word 存储的是指向线程栈中Lock Record 的指针

重量锁:Mark Word 存储的是指向堆中的monitor对象的指针

无锁状态:Mark Word 可以存储 hash code值。当对象的hashCode()方法第一次被调用时,JVM会生成对应的hash code值并存储到Mark Word中。

对于偏向锁:在线程获取偏向锁时,会用Thread ID和epoch值覆盖掉hash code所在的位置。如果一个对象的hashCode() 方法已经被调用过一次之后,这个对象不能被设置偏向锁。因为可以的话,那么Mark Word中的hash code必然会被偏向线程id给覆盖。

对于轻量级锁:JVM会在当前线程的栈帧中创建Lock Record(锁记录空间),用于存储锁对象的Mark Word拷贝。所以轻量级锁与hash code 共存。释放锁后会将信息回写到对象头。

对于重量级锁:代表重量级锁的ObjectMonitor类里有字段记录非枷锁状态下Mark Word,锁释放后也会将信息写回到对象头。

  • 在获取偏向锁之前调用 hashCode() ,会升级成轻量级锁。(也就是 空白Mark Word 先存储hashcode 后在存储线程ID)

  • 在获取偏向锁之后调用 hashCode(),会升级成重量级锁。(也就是 空白Mark Word先存储线程ID后又想存储hashCode)

小总结

先自旋,不行再阻塞。

如果同步代码块执行时间过长,那么轻量级锁自旋带来的性能消耗就比使用重量级锁更加严重。

JIT编译器对锁的消除

锁消除

下面代码发生了锁消除,因为每个线程都自己new了一个对象作为自己的锁,这样是没有意义的(应是多个线程抢同一把锁),jit编译器会自己忽略它(逃逸分析)。

public class SynchronizedDemo1 {
    static Object staticObject = new Object();

    public void m1(){
        Object o = new Object();
        synchronized (o){
            System.out.println(Thread.currentThread().getName()+"\t"+o.hashCode()+"\t"+staticObject.hashCode());
        }
    }

    public static void main(String[] args) {
        SynchronizedDemo1 synchronizedDemo1 = new SynchronizedDemo1();
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                synchronizedDemo1.m1();
            },String.valueOf(i)).start();
        }

    }
}

锁粗化

方法中前后相邻都是同一个锁对象,JIT会把几个synchronized合并成一个大块。

一次申请和释放锁,提高了性能。

        new Thread(() -> {
            synchronized (o) {
                System.out.println(111);
            }
            synchronized (o) {
                System.out.println(222);
            }
            synchronized (o) {
                System.out.println(333);
            }
            synchronized (o) {
                System.out.println(444);
            }
        }).start();
        new Thread(() -> {
            synchronized (o) {
                System.out.println(111);
                System.out.println(222);
                System.out.println(333);
                System.out.println(444);
            }
        }).start();
  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值