synchronized的锁升级

synchronized

synchronized具体实现,这个是在JVM内部完成的

  1. 开始使用的时候是乐观锁,如果发现锁的冲突率比较高,就会自动转换为乐观锁
  2. synchronized不是读写锁
  3. synchronized开始的时候是轻量级锁,如果锁被持有的时间较长/锁的冲突概率较高,就会升级成重量级锁
  4. synchronized是一个非公平锁
  5. synchronized是一个可重入锁
  6. synchronized为轻量级锁的时候,大概率是一个自旋锁;为重量级锁的时候大概率是一个挂起等待锁

synchronized的锁升级

synchronized锁一共有4种状态,从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,这几个状态会随着竞争情况逐渐升级

偏向锁

偏向锁,其实也就是一种乐观锁。
大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获取锁的代价更低从而引入了偏向锁。
偏向锁,偏向于第一个获取到锁的线程,当第一个线程获取到锁时(第一次访问synchronized代码块),会在对象头里记录当前偏向的线程ID,后面如果这个线程再次访问就不小进行CAS操作来加锁和解锁,只需要看看对象头里的锁是否是自己持有(对象头里的ID是否是自己),如果是就直击往下执行,就不需要其它操作效率是非常高的。
偏向锁的撤销
偏向锁只有当其它线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等到一个合适的时机,也就是加锁的代码没有在执行。它会首先暂停持有偏向锁的线程,并检测持有偏向锁的线程是否存活,如果线程不处于存活状态,则将对象头设置为无锁状态。

偏向锁只是在对象头中设置了一个“偏向锁标记”,这个只做标记,就比真正的加锁,要高效很多。
偏向锁本质上相当一一种"延时加锁",完全没有竞争的时候,是偏向锁

轻量级锁

轻量级锁是指当锁是偏向锁的时候,却被另外的线程所访问,此时偏向锁就会升级为轻量级锁,其
他线程会通过自旋的形式尝试获取锁,线程不会阻塞,从而提高性能。(这里需要说明一下锁竞争:如果多个线程轮流获取一个锁,但是每次获取锁的时候很顺利,没有发生阻塞,那么就不存锁竞争了,只有当某个线程尝试获取锁的时候,发现锁已经被占用,只能等待其释放锁,这才发生了锁竞争)。

在轻量级锁状态下继续竞争,没有抢到锁的线程将自旋,即不停地循环判断锁是否能够被成功获取,获取锁的操作,其实就是通过CAS修改对象头的锁标志位。先比较当前锁标志位是否为“释放”,如果是则将其设置为“锁定”,这就算抢到锁了,然后线程将当
前锁的持有者信息修改为自己

此处的轻量级锁,就是基于CAS实现的自旋锁,是属于完全在用户态完成的操作,因此这里不涉及到内核态用户态的切换,也不涉及到线程的阻塞等待和调度,只是多浪费了一些CPU而已。
但是如果当前的场景,是锁的冲突比较大,锁的竞争比较激烈。此时锁还会进一步的膨胀成重量级锁

重量级锁

如果锁冲突率太大了,轻量级自旋锁,就会浪费大量的CPU(在等待的时候CPU是空转的),自旋本质就是为了减少用户态和内核态之间切换的开销,但是竞争锁的线程多了就会疯狂自旋,占用大量的CPU的资源。
使用更重量的挂起等待锁,就可以解决这个问题。当锁升级为重量级锁后,当有线程来获取锁发现锁已经被其它线程持有,就会挂起阻塞等待,这就涉及到了用户态和内核态的切换,将线程的控制权交给了操作系统进行调度,频繁的修改线程的状态(线程的挂起和唤醒),从而消耗大量的系统资源。

对于挂起等待锁来说,当锁等待的过程中,是释放CPU(不会占用CPU资源)

代价就是引入了线程的阻塞和调度开销

锁的升级(偏向锁 ——> 轻量级锁 ——> 重量级锁)

锁消除

官方的JDK1.8中是没有实现锁降级的,但是synchronized有一种优化手段,锁消除

锁消除,其实就是编译器和JVM自行判定一下,看着当前这个代码是否真的需要加锁。JVM和编译器,会对代码中的synchronized进行简单的判断,如果这个加锁没有必要。如果每必要就算程序员加了锁,编译器也会自动的把锁给去掉。但这个优化不能保证每个没必要加锁的地方,都被优化掉。
比如只有一个线程(或者有多个线程),多个线程不涉及修改同一个变量,如果代码中写了synchronized,此时synchronized加锁操作,就会直接被JVM给干掉。锁消除也是一种编译器优化的行为,编译器的判定不一定非常准,因此如果代码的锁100%能消除,就会消除吗,反之判定80%是能消除的但还是需要考虑极端情况,是不能消除的。锁消除只是在编译器/JVM有十足的把握的时候才进行的

锁的粗化

锁的粒度,就是看synchronized代码块中包含了多少代码

如果包含的代码多,认为锁的粒度比较粗。

如果包含的代码少,认为锁的粒度比较细。

一般情况下还是把加锁的代码写细一点比较好,比如我们一把锁需要保护多个相互独立的操作,那么我们就可以将这个锁分解成多个锁。如果锁的粒度细,意味着代码持有锁的时间就短,就能更快释放,这样其它线程冲突的概率就更低。尽量少加锁,因为加锁会带来一定的开销。

但是有的情况还是写粗一点更好

public void func() {
    synchronized (this) {
        //任务1
    }
    synchronized (this) {
        //任务2
    }
    synchronized (this) {
        //任务3
    }
}

这种写法还不如

public void func() {
    synchronized (this) {
        //任务1
        //任务2
        //任务3
    }
}

就算程序员不这么写,JVM和编译器也会进行只能判定,会把这个情况的多个synchronized合并成一组

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱敲代码的三毛

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值