Java 原理:synchronized 机制与几类锁

本贴最后更新于 2101 天前,其中的信息可能已经事过景迁

synchronized 机制

synchronized 关键字是 JAVA 中常用的同步功能,提供了简单易用的锁功能。
synchronized 有三种用法,分别为:

  • 用在普通方法上,能够锁住当前对象。
  • 用在静态方法上,能够锁住类
  • 用在代码块上,锁住的是 synchronized()里的对象

在 JDK6 之前,synchronized 使用的是重量级锁制,在之后 synchronized 加入了锁膨胀机制,显著提升了 synchronized 关键字的效率。

基于 synchronized 关键字,我们来了解下几种类别的锁,同时立即 synchronized 的锁膨胀机制。

synchronized 锁是非公平锁

一个被 synchronized 锁住的对象或类,就是一把锁。

另外一提,所有锁都是存储在 Java 对象头里的,Java 对象头里的 Mark Word 里默认存储对象的 HashCode,分代年龄和锁标记位。也就是说 Mark Word 记录了锁的状态

锁膨胀机制与几类锁

锁膨胀是不可逆的

偏向锁

synchronized 在 JDK1.6 以后默认开启 偏向锁synchronized 最初都是 偏向锁

表现:一个线程获取锁成功后,会在对象头里记录线程 ID,以后该线程获取和释放锁都没有任何花费。(因为该锁已经被绑定在该线程上了,且在膨胀前不会改变),如果其他线程尝试获取这个锁,偏向锁 将会膨胀为 轻量锁

优点:在只有一个线程使用锁的时候获取和退出锁没有任何花费

缺点:锁竞争激烈会很快升级为 轻量锁,那么维持 偏向锁 的过程就是在浪费计算机资源。(因为 偏向锁 本身就很轻量,因此浪费的资源并不多)

小结:只有一个线程使用锁的情况下,synchronized 使用的锁为 偏向锁
如果锁竞争激烈,可以通过配置 JDK 禁用 偏向锁

轻量锁

一把锁不止一个线程使用,则 偏向锁 膨胀为 轻量锁

表现:线程获取 轻量锁 时,会直接用 CAS 修改对象头里锁的记录,如果修改失败,代表此时锁存在多个线程的竞争,轻量锁 将会膨胀为 重量锁

优点:在线程之间使用锁不存在竞争时,一次 CAS 操作就能获取和退出锁

缺点:与 偏向锁 类似

小结:只要一把锁不止一个线程获取过,偏向锁 就会膨胀为 轻量锁

重量锁

一把锁存在多线程竞争,则 轻量锁 开始自旋,自旋一定次数后仍没获取锁,则膨胀为 重量锁

表现:线程获取 重量锁 时,如果获取失败(即锁已被其他线程获取),则使用 自适应自旋锁,自旋一定次数后仍没获取锁,则进入阻塞队列等待。

优点:未获取到的锁进入阻塞队列,节约 CPU 资源。(好吧感觉其实是没有啥优点)

缺点:重量锁 是通过对象内部的监视器(monitor)实现,其中 monitor 的本质是依赖于底层操作系统的 Mutex Lock 实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。

小结:只要一把锁存在多线程竞争,轻量锁 就会膨胀为 重量锁

自旋锁

synchronized轻量锁重量锁,使用了 自适应自旋锁 进行性能优化

首先介绍 自旋锁

表现:线程获取锁失败后,不会进入阻塞等待,而是再次尝试去获取锁,如此反复,直到获取到锁,或者自旋结束那么会阻塞等待。

解决问题:在某些场景下,线程持有锁的时间非常短。在线程获取锁失败后,如果线程进入阻塞将会带来线程上下文的切换,上下文切换的时间可能反而高于线程反复尝试获取锁的时间。
此时线程原地等待去重复获取锁。反而在性能上更有优势。

缺点:

  1. 单核 CPU 没有线程并行,反复尝试会导致进程无法继续运行。
  2. 重复尝试导致了 CPU 的占用,如果 CPU 资源紧张的话反而会性能下降
  3. 如果锁的竞争时间过长,不仅没有性能提升,还浪费了大量 CPU 资源。

优化:使用 自适应自旋锁。自适应自旋锁会根据之前的锁获取记录,优化调整自旋时间,避免造成不必要的自旋。

具体 synchronized 流程

  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3168 引用 • 8207 回帖
  • 11 引用 • 8 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...