侧边栏壁纸
博主头像
再见理想博主等级

只争朝夕,不负韶华

  • 累计撰写 112 篇文章
  • 累计创建 64 个标签
  • 累计收到 4 条评论

目 录CONTENT

文章目录

浅谈偏向锁、轻量级锁、自旋锁、重量级锁

再见理想
2022-05-29 / 0 评论 / 0 点赞 / 325 阅读 / 2,711 字

一,偏向锁

偏向锁的目标是,减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗。轻量级锁每次申请、释放锁都至少需要一次CAS,但偏向锁只有初始化时需要一次CAS。

偏向锁的“偏”,就是偏心的“偏”、偏袒的“偏”,它的意思是这个锁会偏向于第一个获得它的线程,会在对象头存储锁偏向的线程ID,以后该线程进入和退出同步块时只需要检查是否为偏向锁、锁标志位以及 ThreadID 即可。

**一个线程的情况适合用偏向锁。**一旦出现多个线程竞争时必须撤销偏向锁,所以撤销偏向锁消耗的性能必须小于之前节省下来的CAS原子操作的性能消耗,不然就得不偿失了。

原理

一个线程在第一次进入同步块时,会在对象头和栈帧中的锁记录里存储锁的偏向的线程 ID。当下次该线程进入这个同步块时,会去检查锁的 Mark Word 里面是不是放的自己的线程 ID。如果是,表明该线程已经获得了锁,以后该线程在进入和退出同步块时不需要花费 CAS 操作来加锁和解锁;

如果不是,就代表有另一个线程来竞争这个偏向锁。这个时候会尝试使用 CAS 来替换 MarkWord 里面的线程 ID 为新线程的 ID,这个时候要分两种情况:
1,成功:表示之前的线程不存在了, Mark Word 里面的线程 ID 为新线程的 ID,锁不会升级,仍然为偏向锁;
2,失败:表示之前的线程仍然存在,那么暂停之前的线程,设置偏向锁标识为并设置锁标志位为 00,升级为轻量级锁会按照轻量级锁的方式进行竞争锁。

优点

偏向锁在资源无竞争情况下消除了同步语句,连 CAS 操作都省略了,提高了程序的运行性能。

偏向锁流程图:


二,轻量级锁

自旋锁的目标是降低线程切换的成本。如果锁竞争激烈,我们不得不依赖于重量级锁,让竞争失败的线程阻塞;如果完全没有实际的锁竞争,那么申请重量级锁都是浪费的。轻量级锁的目标是,减少无实际竞争情况下,使用重量级锁产生的性能消耗,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。

2.1 先了解下 Lock Rcord

在升级轻量级锁之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间即将对象头中用来标记锁信息相关的内容封装成一个 java 对象放入当前线程的栈帧中,这个对象称为 Lock Rcord

栈上分配LockRecord如下图: lockrecord中包含了对象的引用地址

对象头中markword替换锁记录指针成功之后如下图:

替换成功之后将锁标志位改为 00 表示获取轻量级锁成功。

lockrecord 的作用:

在这里实现了锁重入,每当同一个线程多次获取同一个锁时,会在当前栈帧中放入一个 lockrecord,但是重入是放入的 lockrecord 关于锁信息的内容为 null,代表锁重入。当轻量级解锁时,每解锁一次则从栈帧中弹出一个lockrecord,直到为0。
轻量级锁重入之后如下图:

锁膨胀流程,升级为重量级锁:

即为 Object 对象申请 Monitor 锁,让 Object 指向重量级锁地址;然后自己进入 Monitor 的 EntryList BLOCKED。

2.2 原理

线程尝试通过 CAS 将对象头中 mark word 替换为指向锁记录 Lock Rcord 的指针。如果更新成功,则轻量级锁获取成功,记录锁状态为轻量级锁。

否则只能说明该锁对象已经被其他线程抢占了,这时轻量级锁需要膨胀为重量级锁,锁标志位变成10,后面等待的线程将会进入阻塞状态。

2.3 优点

在多线程交替执行同步块的情况下,可以避免重量级锁引起的性能消耗。


三,自旋锁

monitor会阻塞和唤醒线程,线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作,这些操作给系统的并发性能带来了很大的压力。

为了让线程等待,我们只需让线程执行一个自旋,这项技术就是所谓的自旋锁。

自旋等待不能代替阻塞,且先不说对处理器数量的要求,自旋等待本身虽然避免了线程切换的开销,但它是要占用处理器时间的,因此,如果锁被占用的时间很短,自旋等待的效果就会非常好,反之,如果锁被占用的时间很长。那么自旋的线程只会白白消耗处理器资源,而不会做任何有用的工作,反而会带来性能上的浪费。

因此,自旋等待的时间必须要有一定的限度,如果自旋超过了限定的次数仍然没有成功获得锁,就应当使用传统的方式去挂起线程了。自旋次数的默认值是10次,用户可以使用参数 -XX : PreBlockSpin 来 更改。

缺点

1,单核处理器上,不存在实际的并行,当前线程不阻塞自己的话,旧owner就不能执行,锁永远不会释放,此时不管自旋多久都是浪费;进而,如果线程多而处理器少,自旋也会造成不少无谓的浪费。

2,自旋锁要占用 CPU,如果是计算密集型任务,这一优化通常得不偿失,减少锁的使用是更好的选择。

3,如果锁竞争的时间比较长,那么自旋通常不能获得锁,白白浪费了自旋占用的CPU时间。这通常发生在锁持有时间长,且竞争激烈的场景中,此时应主动禁用自旋锁。

使用 -XX:-UseSpinning 参数关闭自旋锁优化;-XX:PreBlockSpin 参数修改默认的自旋次数。

适应性自旋锁

在JDK 6中引入了自适应的自旋锁。自适应意味着自旋的时间不再固定了,而是由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定:

如果在同一个锁对象上,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而它将允许自旋等待持续相对更长的时间,比如100个循环。

相反的,如果对于某个锁,自旋很少成功获得过,那在以后要获取这个锁时将可能减少自旋时间甚至省略自旋过程,以避免浪费处理器资源。

自适应自旋解决的是 锁竞争时间不确定 的问题。JVM 很难感知到确切的锁竞争时间,而交给用户分析就违反了 JVM 的设计初衷。自适应自旋假定不同线程持有同一个锁对象的时间基本相当,竞争程度趋于稳定,因此,可以根据上一次自旋的时间与结果调整下一次自旋的时间。


四,重量级锁

内置锁在Java中被抽象为监视器锁 monitor。在JDK 1.6之前,监视器锁可以认为直接对应底层操作系统中的互斥量 mutex。这种同步方式的成本非常高,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等,成本非常高。因此,后来称这种锁为重量级锁。

Synchronized 之重量级锁的实现原理


五,锁消除

虚拟机会根据自己的代码检测结果取消一些加锁逻辑。虚拟机通过检测会发现一些代码中不可能出现数据竞争,但是代码中又有加锁逻辑,为了提高性能,就消除这些锁。如果一段代码中,在堆上的所有数据都不会被其他线程访问到,那就可以把它们当成线程私有数据,自然就不需要同步加锁了。

JIT 编译器可以在动态编译同步代码时,使用一种叫做逃逸分析的技术,来通过该项技术判别程序中所使用的锁对象是否只被一个线程所使用,而没有散布到其他线程当中;如果情况就是这样的话,那么 JIT 编译器在编译这个同步代码时就不会生成 synchronized 关键字标识的锁的申请和释放机器码,从而消除了锁的使用流程。


六,锁粗化

每一个synchronized块都对应一个 monitorenter 和两个 monitorexit,JIT 编译器在执行动态编译时会对上面代码进行优化:若发现前后相邻的 synchronized 块使用的是同一个锁对象,那么它就会把这几个 synchronized 块给合并为一个较大的同步块,这样做的好处在于线程在执行这些代码时,就无需频繁申请与释放锁了,从而达到申请与释放锁一次,就可以执行完全部的同步代码块,从而提升了性能。


七,各种锁的优缺点比较

0

评论区