从 Synchronized 到锁的优化

作者 : 开心源码 本文共10068个字,预计阅读时间需要26分钟 发布时间: 2022-05-14 共168人阅读

我们知道 SynchronizedJava 中处理并发问题的一种最常用的方法, 也是最简单的一种方法. 被也被称为内置锁.

Synchronized 的作用主要有三个:

  • 确保线程互斥的访问同步代码
  • 保证共享变量的修改能够及时可见
  • 有效处理重排序问题。

 
从语法上讲, Synchronized 总共有三种用法:

  • 修饰普通方法, 锁是当前实例对象.
  • 修饰静态方法, 锁是当前类的 class 对象.
  • 修饰代码块, 锁是括号中的对象.

关于使用方式, 这里就不再进行逐个形容了. 我们直接进入正题, 看 Synchronized 的底层实现原理是什么.

1. Synchronized 原理

首先, 我们先来看一段代码, 使用了同步代码块和同步方法, 通过使用 javap 工具查看生成的 class 文件信息来分析 synchronized 关键字的实现细节.

代码片段
对代码进行反编译后的结果如下

  public static void main(java.lang.String[]) throws java.lang.Exception;    descriptor: ([Ljava/lang/String;)V    flags: (0x0009) ACC_PUBLIC, ACC_STATIC    Code:      stack=2, locals=3, args_size=1         0: getstatic             3: dup         4: astore_1         5: monitorenter  //---------------------------------------------1.         6: aload_1         7: monitorexit    //---------------------------------------------2.         8: goto          16        11: astore_2        12: aload_1        13: monitorexit   //---------------------------------------------3.        14: aload_2        15: athrow        16: return        ...  public static synchronized void test();    descriptor: ()V    flags: (0x0029) ACC_PUBLIC, ACC_STATIC, ACC_SYNCHRONIZED //---------------------------------------------4.    Code:      stack=0, locals=0, args_size=0         0: return      LineNumberTable:        line 21: 0

从生产的 class 信息中, 可以清楚的看到两部分内容

  • 同步代码块中使用了 monitorentermonitorexit 指令.
  • 同步方法中依靠方法修饰符 flags 上的 ACC_SYNCHRONIZED 实现.

先看反编译出 main 方法中标记的 1 与 2. monitorenter / monitorexit 关于这两条指令的作用, 参考 JVM 中对他们的形容如下:

monitorenter
每个对象有一个监视器锁 monitor, 当 monitor 被占用时就会处于锁定状态, 线程执行 monitorenter 指令时尝试获取 monitor 的所有权, 过程如下

  • 假如 monitor 的进入数为 0 , 则该线程进入 monitor, 而后将进入数设置为 1, 该线程即为 monitor 的拥有者.
  • 假如线程已经占有该 monitor, 只是重新进入, 则进入 monitor 的进入数加 1.
  • 假如其余线程已经占用了 monitor, 则该线程进入阻塞状态, 直到 monitor 的进入数为 0, 再尝试获取 monitor 的所有权.

 
monitorexit
执行 monitorexit 的线程必需是对应 monitor的所有者. 执行指令时, monitor的进入数减 1. 假如减 1 后进入数为 0, 则线程退出 monitor. 不再是这个 monitor 的所有者. 其余被这个 monitor 阻塞的线程可以尝试去获取这个 monitor 的所有权.

 

monitorenter 指令是在编译后插入到同步代码块开始的位置, 而 monitorexit 是插入到方法的结束处和异常处. 这也就是为什么在 3 处会单独有一个 monitorexit 了.

 

ACC_SYNCHRONIZED
当方法调用时, 调用指令将检查方法的 ACC_SYNCHRONIZED 访问标志能否被设置, 假如设置了, 执行线程将先获取 monitor, 获取成功之后才能执行方法体. 方法执行完后再释放 monitor, 在方法执行期间, 其余任何线程都无法再取得同一个 monitor 对象.
其实这个和上面 monitorentermonitorexit 本质上没有区别, 只是方法的同步是一种隐式的方式来实现的, 无需通过字节码来完成.

看完这些, 是不是觉得有点和 AQS 中的 state 类似? 假如看完了 从 LockSupport 到 AQS 的简单学习 这篇文章的朋友, 再来看这里, 我相信应该会很容易了解.

这里既然说到了监视器锁 monitor , 我们一起来看这究竟是什么.
 


2. 监视器锁 monitor

监视器锁 monitor 本质是依赖于底层操作系统的 Mutex Lock (互斥锁) 来实现的. 每个对象都对应于一个可称为 “互斥锁” 的标记, 这个标记用来保证在任一时刻, 只能有一个线程访问该对象.

以下是 Mutex 的工作方式

Mutex 工作流程

  • 申请 Mutex.
  • 假如成功, 则持有该 Mutex.
  • 假如失败, 则进行自旋, 自旋的过程就是在等待 Mutex, 不断发起 Mutex gets, 直到获取 Mutex 或者者达到自旋次数的限制为止.
  • 依据工作模式的不同选择 yiled 还是 sleep.
  • 若达到 sleep 限制或者者主动唤醒,又或者者完成 yiled, 则继续重复上面 4 点, 直到取得 Mutex 为止.

 

3. 为什么说 Synchronized 是重量级锁?

Synchronized 是通过对象内部的一个叫监视器锁 monitor 来实现的, 监视器锁本质又是依赖于底层的操作系统的互斥锁 Mutex Lock 来实现的.

而从 Mutex Lock (互斥锁) 的工作流程我们可以得知是自旋和阻塞, 既然是阻塞那么一定有唤醒. 因为 Java 的线程是映射到操作系统的原生线程之上的, 所以说假如要阻塞或者者唤醒一条线程, 都需要操作系统来帮忙完成, 这就需要从客户态转到内核态. 这个成本非常高, 状态之间的转换需要相对较长的时间, 因而状态转换需要消耗很多解决器时间. 这就是为什么 Synchronized 效率低的起因. 因而, 这种依赖于操作系统互斥锁 Mutex Lock 所实现的锁, 我们称之为 “重量级锁”.

But, 在 JDK1.6 中为了取得锁和释放锁带来的性能消耗, 引入了 偏向锁轻量级锁, 使得 SynchronizedReentrantLock 的性能基本持平. ReentrantLock 只是提供了比 Synchronized 更丰富的功能, (比方尝试获取锁,尝试释放锁等) 而不肯定有更优的性能, 所以在 Synchronized 能实现需求的情况下, 尽量还是优先考虑使用 Synchronized 来进行同步.

锁一共有 4 种状态: 级别从低到高依次是: 无锁状态 -> 偏向锁状态 -> 轻量级锁状态 -> 重量级锁状态
锁可以更新, 但是不能降级.

在 JDK1.6 中, 除了引入偏向锁与轻量级锁的概念, 还有锁消除, 锁粗化等等.

接下来我们理解锁是如何优化前, 需要先理解一个重要的概念, 那就是 java 对象头.

 

4. java 对象头

Synchronized 锁是存在 java 对象头中的, 那么什么是 java 对象头呢?
Hotspot 虚拟机中, 对象在内存的分布为三个部分, 头像头, 实例数据, 对齐填充. 而对象头主要包括

  • Mark Word (标记字段) : 用于存储对象自身的运行时数据, 如哈希码, GC分代年龄, 锁状态标志, 线程持有的锁, 偏向线程 ID, 时间戳等等.
  • Klass Pointer (类型指针) : 存储对象的类型指针,该指针指向它的类元数据

Hotspot 虚拟机: JVM 虚拟机, 总的来说是一种标准规范, 虚拟机有很多实现版本. 主要作用就是运行 java 的类文件的. 而 Hotspot 虚拟机是虚拟机的一种实现, 它是 SUN 公司开发的, 是 sun jdk 和 open jdk 中自带的虚拟机, 同时也是目前使用范围最广的虚拟机.

Hotspot 与 JVM 两者的区别一个是实现方式, 一个是标准.

额外知识点 : Java 对象头一般占有 2 个机器码(在 32 位虚拟机中, 1 个机器码等于 4 字节, 也就是 32 bit), 但是假如对象是数组类型, 则需要 3 个机器码, 由于 JVM 虚拟机可以通过 Java 对象的元数据信息确定 Java 对象的大小, 但是无法从数组的元数据来确认数组的大小, 所以用一块来记录数组的长度. 下图是Mark Word 默认的存储结构 (32 位虚拟机)

Mark Word 默认结构

对象头信息是与对象自身定义的数据无关的额外存储成本, 但是考虑到虚拟机的空间效率, Mark Work 被设计成一个非固定的数据结构, 以便在极小的空间内存储更多的信息. 也就是说, Mark Word 会随着程序的运行发生变化, 变化状态如下(32 位虚拟机)

Mark Word 的状态变化

我们现在知道锁的状态及相关信息是存在了 java 对象头中的 Mark Word 中. 接着来看下锁是如何优化的. 无锁状态就不再说了, 我们从最低的偏向锁开始.

 

5. 锁优化 – 偏向锁

什么是偏向锁
偏向锁, 顾名思义, 它会偏向于第一个访问锁的线程. 假如在运行过程中只有一个线程访问同步块, 会在对象头和栈帧中的锁记录里存储当前线程的 ID, 以后该线程在进入和退出同步块时不需要进行 CAS 操作来加锁和解锁, 只要要简单的判断一下对象头的 Mark Word 里能否存储着当前线程的ID.

 
为什么要引入偏向锁
经过研究发现, 在大多数情况下, 锁不仅不存在多线程竞争, 而且总是由同一线程屡次取得, 为了让线程取得锁的代价更低而引入了偏向锁, 减少不必要的 CAS 操作, 从而提高性能.

引入偏向锁是为了在无多线程竞争的情况下尽量减少不必要的轻量级锁执行路径, 由于轻量级锁的获取和释放依赖屡次的 CAS 原子指令. 而偏向锁只要要在置换线程 ID 的时候依赖一次 CAS 原子指令. 由于一旦出现多线程竞争的情况就必需撤掉偏向锁, 膨胀为轻量级锁. 所以偏向锁的撤销操作的性能损耗必需小于节省下来的 CAS 原子指令的性能消耗.

 
偏向锁的三种状态

  • 匿名偏向: 这是允许偏向锁的初始状态, 其 Mark Word 中的 Thread ID 为0, 第一个试图获取该对象锁的线程会遇到这种状态, 可以通过 CAS 操作修改 Thread ID 来获取这个对象的锁.
  • 可重偏向: 这个状态下 Epoch 是无效的, 下一个线程会遇到这种情况, 在批量重偏向操作中, 所有未被线程持有的对象都会被设置成这个状态. 而后在下个线程获取的时候能够重偏向. (批量重偏向这里不深入分析, 有兴趣的可以执行研究一下)
  • 已偏向: 这个状态最简单, 就是被线程持有着, 此时 Thread ID 为其偏向的线程.

假如 JVM 启用偏向锁, 那么一个新建未被任何线程获取的对象头 Mark Word 中的 Thread Id 为0, 是可以偏向但未偏向任何线程, 被称为匿名偏向状态. 而无锁状态是不可偏向也未偏向任何线程, 不可再变为偏向锁. 记住!无锁状态不能变成偏向锁!

 
偏向锁获取过程

  1. 在一个线程进入同步块的时候, 检测 Mark Word 中能否为可偏向状态, 即 [能否是偏向锁] = 1, [锁标志位] = 01
  2. 若为可偏向状态, 检测 Mark Word中记录的线程 ID 能否是当前线程 ID, 假如是执行步骤 5, 不是执行步骤 3. 若为不可偏向状态, 直接执行轻量级锁流程.
  3. 假如线程ID并未指向当前线程,则通过 CAS 操作竞争锁, 竞争成功则将 Mark Word 中线程 ID 设置为当前线程 ID. 而后执行步骤 5, 否则执行步骤 4.
  4. 通过 CAS 获取偏向锁失败, 则表示有竞争. (CAS 获取偏向锁失败说明至少有过其余线程曾取得过偏向锁, 由于线程不会主动释放偏向锁). 当到达全局安全点 (safepoint) 时, 会首先挂起拥有偏向锁的线程, 而后检查持有偏向锁的线程能否还活着, (由于有可能持有偏向锁的线程已经执行完毕, 但是该线程不会主动释放偏向锁)
    • 假如还存活, 接着判断能否还在同步代码块中执行.
      • 若还在同步代码块中执行, 直接更新为轻量级锁.
      • 若未在同步代码块中执行, 则看能否可重偏向,
        • 不可重偏向: 直接撤销偏向锁, 变为无锁状态后, 更新为轻量级锁.
        • 可重偏向: 修改 Mark Word为匿名偏向状态, 通过 CAS 将新线程 ID给 Mark Word 赋值.唤醒新线程, 执行同步代码块.
    • 假如不存活, 也需要判断能否可重偏向.
      • 不可重偏向: 直接撤销偏向锁, 变为无锁状态后, 更新为轻量级锁.
      • 可重偏向: 修改 Mark Word为匿名偏向状态, 通过 CAS 将新线程 ID给 Mark Word 赋值.唤醒新线程, 执行同步代码块.

JVM 维护了一个集合存放所有存活的线程, 通过遍历该集合判断能否有线程的 ID 等于持有偏向锁线程的 ID, 有的话表示存活.

 
至于能否还在同步块中执行: 这个就需要说到锁记录 Lock Record

当代码进入同步块的时候, 假如此时同步对象未被锁定 (即 [锁标志位] = 01) , 虚拟机会在当前线程的栈帧中新建一个空间, 来存放锁记录 Lock Record , 锁记录用于存储记录目前对象头 Mark Word 的拷贝 (官方称之为 Displaced Mark Word) 以及记录锁对象的指针 owner.

栈帧: 这个概念涉及的内容较多, 不便于开展叙述. 从了解下文的角度上来讲, 只要要知道, 每个线程都有自己独立的内存空间, 栈帧就是其中的一部分. 里面可以存储仅属于该线程的少量信息.

在偏向锁时也有 Lock Record 存在, 只不过作用不大. Lock Record 主要用于轻量级锁和重量级锁.


 
锁记录可以做什么?
可以统计重入的次数, 判断当先线程能否还在同步块中执行.以及在轻量级锁中会大量用到.

统计重入次数
线程每次进入同步块(即执行monitorenter)都会新建一个锁记录, 并将新建锁记录中的 Displaced Mark Word 设为 null . 用来作为统计重入的次数. owner 指向当前的锁对象.

每次解锁 (即执行monitorexit) 的时候都会从最低的一个相关的锁记录移除. 所以可以通过遍历线程栈中的Lock Record来判断线程能否还在同步块中.

下图是一个重入三次的 Lock Record 示用意.

轻量级锁重入

为什么 JVM 选择在线程栈帧中增加 Displaced Mark WordnullLock Record 来表示重入计数而不是将重入次数直接放在对象头的 Mark Word 中呢. 之前说过, Mark Word 的大小是有限制的, 已经存不下该信息了.

那么为什么不只创立一个锁记录在其中记录重入次数呢? 这点我也没有想明白. 假如有知道答案的朋友, 请留言告知一下, 万分感谢 !!!
 

 
偏向锁的撤销过程
偏向锁的撤销在上面第 4 点有说到, 偏向锁使用了一种等到竞争出现才释放偏向锁的机制: 偏向锁只有遇到其余线程尝试竞争偏向锁时, 持有偏向锁的线程才会释放, 线程本身不会主动去释放偏向锁. 偏向锁的撤销需要等待全局安全点(在这个时间点上没有字节码正在执行), 它会首先暂停拥有偏向锁的线程, 判断锁对象能否处于被锁定状态, 撤销偏向锁后恢复到无锁或者轻量级锁的状态. 我们发现, 这个开销其实还是挺大的, 所以假如某些同步代码块大多数情况下都是有两个及以上的线程竞争的话, 那么偏向锁就会值一种累赘, 对于这种情况, 建议一开始就把偏向锁关闭.

注意: 偏向锁撤销是指在获取偏向锁的过程中因不满足条件导致要将锁对象改为非偏向锁状态, 而偏向锁释放是指退出同步块时的过程.

 
关闭偏向锁
偏向锁在 JDK 6JDK 7 中默认启动的. 因为偏向锁是为了在只有一个线程执行同步块的时候提高性能. 假如能确定应用程序里所有的锁通常情况下处于竞争状态, 可以通过 JVM 参数关闭偏向锁. 那么程序默认会进入轻量级锁的状态.

  • 开启偏向锁: -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
  • 关闭偏向锁:-XX:-UseBiasedLocking

 
偏向锁流程图

偏向锁流程 (1).png

 

6. 锁优化 – 轻量级锁

轻量级锁是由偏向锁更新来的, 偏向锁运行在一个线程进入同步块的情况下, 当有第二个线程进入产生锁竞争的情况下, 就会自动更新为轻量级锁. 其余线程会通过自旋的形式尝试获取锁, 线程不会阻塞, 从而提高性能.

轻量级锁的获取主要有两种情况

  • 当关闭偏向锁功能时
  • 因为多个线程竞争偏向锁导致偏向锁更新为轻量级锁.
     

轻量级锁获取过程

  1. 拷贝对象头中的 Mark Word 到当前线程栈帧的锁记录中. 并且虚拟机通过使用 CAS 操作尝试将对象头的 Mark Word 升级为指向锁记录的指针, 并将锁记录里的 owner 指针指向锁对象. 这个操作成功执行步骤 2, 失败执行步骤 3.

  2. 假如这个升级动作成功了, 那么当前线程就拥有了该对象的锁. 并且对象头的 Mark Word 的锁标志位改为 00, 即表示此对象处于轻量级锁定状态, 这时候线程堆栈与对象头的状态如下图所示.

  1. 假如这个升级操作失败了, 虚拟机首先会检查对象的 Mark Word 能否指向当前线程的栈帧
    • 假如不是: 说明这个锁对象已经被其余线程抢占了, 则通过自旋略微等待一下, 有可能持有锁的线程很快就会释放锁.
      但是当自旋超过肯定次数(默认允许自旋 10 次, 可以通过虚拟机参数更改), 或者者一个线程在持有锁, 一个在自旋, 又有第三个线程来竞争的时候, 就会膨胀为重量级锁. 除了持有锁的线程外, 其余线程阻塞. 对象头Mark Word 中指向锁记录的指针改为指向重量级锁(互斥量)的指针, 同时将锁标志位改为 10.
    • 假如是: 说明当前线程已经拥有了这个对象的锁, 现在是重入状态. 可直接进入同步块继续执行. 同时会增加一条锁记录 Lock Record, 其中 Displaced Mark Wordnull, 起到一个重入计数器的作用.

 

轻量级锁解锁过程

  1. 遍历当前线程栈帧, 找到所有 owner 指向当前锁对象的 Lock Record
  2. 假如 Lock RecordDisplaced Mark Wordnull 说明这是一次重入, 删除此锁记录, 接着 continue . 这即为一次解锁结束.
  3. 假如 Displaced Mark Word 不为 null, 并且对象头中的 Mark Word 依然指向当前线程的锁记录, 那就通过 CAS 操作把对象头中的 Mark Word 恢复成为 Lock Record 中拷贝过去的 Displaced Mark Word 值.
  4. 假如替换成功, 则 continue. 也即为一次解锁结束.
  5. 假如替换失败. 说明外面有一个线程到达了自旋的总次数, 或者者外面至少还有两个线程来竞争锁, 导致锁已经膨胀为重量级锁. 从而改变了对象头中 Mark Word 的内容. 那就要在释放锁的同时, 唤醒被挂起的线程. 重新争夺锁访问同步块.

轻量级锁能提升程序同步性能的依据是 “对于绝大部分锁在整个同步周期内都是不存在竞争的” 这是一个经验数据. 假如没有竞争, 轻量级锁使用 CAS 操作避免了使用互斥量的开销, 但是假如存在竞争, 除了互斥量的开销外, 还额外发生了 CAS 操作. 因而在有竞争的情况下, 轻量级锁会比传统的重量级锁更慢.

 

7. 锁优化 – 重量级锁

重量级锁就是我们常说的传统意义上的锁, 其利用操作系统底层的同步机制去实现 Java 中的线程同步.
下图是整个偏向锁到轻量级锁再膨胀为重量级锁的流程图. 可能不是很清晰.

锁更新流程

 

8. 锁优化 – 锁消除

何为锁消除?
锁消除即删除不必要的加锁操作, 在详情这个之前, 先说说 逃逸和逃逸分析.

逃逸是指在方法之内创立的对象, 除了在方法体之内被引用之外, 还在方法体之外被引用. 也就是说在方法体之外引用方法内的对象, 在方法执行完毕后, 方法中创立的对象应该被 GC 回收. 但是因为该对象被其余变量引用, 导致 GC 无法回收.

这个无法被回收的对象成为 “逃逸” 对象. Java 中的逃逸分析, 就是对这种对象的分析.

那么接着回到锁消除, Java JIT Java 即时编译 会通过逃逸分析的方式, 去分析加锁的代码能否被一个或者者多个线程使用, 或者者等待被使用. 假如分析证明, 只有一个线程访问, 在编译这个代码段的时候, 就不会生成 Synchronized 关键字, 仅仅生代码对应的机器码.

换句话说, 即便我们开发人员加上了 Synchronized , 但是只需 JIT 发现这段代码只会被一个线程访问, 也会把Synchronized 去掉.

 

9. 锁优化 – 锁粗化

假如一系列的连续操作都对同一个对象反复加锁和解锁,甚至加锁操作是出现在循环体中的,那即便没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。

假如虚拟机检测到有一串零碎的操作都是对同一对象的加锁,将会把加锁同步的范围扩展(粗化)到整个操作序列的外部.

    public static void main(String[] args) throws Exception {        synchronized (object) {            test();        }        // 中间可穿插其余代码        synchronized (object) {            test1();        }        synchronized (object) {            test2();        }    }

上面代码存在三块代码段, 分割成三个临界区, 在 JIT 编译时会将其合并成一个临界区. 用一个锁对其进行访问控制. 减少了锁的获取和释放的次数. 编译后的等效代码如下

    public static void main(String[] args) throws Exception {        synchronized (object) {            test();            test1();            test2();        }    }

锁粗化默认是开启的。假如要关闭这个特性可以在 Java 程序的启动命令行中增加虚拟机参数-XX:-EliminateLocks.

 

10. 锁优化 – 自旋锁与自适应自旋锁

自旋锁的来由
自旋锁我们都知道是为了让该线程执行一段无意义的自旋, 等待一段时间, 不会被立刻挂起, 看持有锁的线程能否会很快释放.

可是为什么要引入自旋锁呢?

首先互斥同步对性能最大的影响就是上面我们说过的阻塞的实现, 由于阻塞和唤醒线程的操作都需要由客户态转到内核态中完成, 这些操作给系统的并发性能带来很大压力.

其次虚拟机的开发团队也注意到许多应用上面, 共享数据的锁定状态只会持续很短一段时间, 为了这一段很短的时间频繁的阻塞唤醒线程非常不值得. 于是, 就引入了自旋锁.

自旋锁的缺点
自旋锁尽管可以避免线程切换带来的开销, 但是它却占用了解决器的时间. 假如持有锁的线程很快就释放了锁, 那么自旋的效率就非常好. 反之, 自旋的线程就会白白白费解决器的资源带来性能上的白费. 所以说自旋的次数必需要有一个限度, 例如 10 次. 假如超过这个次数还未获取到锁, 则就阻塞.

理解了自旋锁, 那自适应的自旋锁呢?

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

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

假如对于某个说, 自旋很少成功过, 那么在以后要获取这个锁时将可能省略掉自旋的过程, 以避免白费解决器资源.

简单来说, 就是线程假如自旋成功了, 则下次自旋的次数会更多, 假如自旋失败了, 则自旋的次数减少.

 

OK, 到这里也差不多了, 关于锁的优化基本就分析完了, 最后来个总结吧.

11. 总结

偏向锁轻量级锁重量级锁
本质取消同步操作CAS 操作代替互斥同步互斥同步
优点不阻塞, 执行效率高, 只有第一次获取偏向锁时需要 CAS 操作, 后面只要要比照线程 ID不会阻塞不会空耗 CPU
缺点适用场景太局限, 若产生竞争, 会有额外的偏向锁撤销的消耗自旋会白费 CPU 资源阻塞唤醒, 客户态切换到内核态. 重量级操作
  • synchronized 的特点: 保证了内存可见性, 操作的原子性.
  • synchronized 影响性能的起因
    • 加锁和解锁需要额外操作
    • 互斥同步对性能最大的影响就是阻塞的时间, 由于阻塞唤醒会由客户态转到内核态中完成. 代价太大.

偏向锁, 轻量级锁, 重量级锁都是 java 虚拟机自己内部实现, 当执行到 synchronized 同步代码块的时候, java 虚拟机会根据启用的锁和当前线程的争用情况来决定如何执行同步操作.

在所有的锁都启用的情况下线程进入临界区时会先取得偏向锁, 假如已经存在偏向锁了, 则会尝试获取轻量级锁, 启用自旋锁, 假如自旋也没获取到锁, 则使用重量级锁, 没有获取到锁的线程被阻塞挂起, 知道持有锁的线程执行完同步代码块后去唤醒它们.

假如线程争用激烈, 那么应该禁用偏向锁.

不同的锁有不同特点, 每种锁只有在其特定的场景下, 才会有出色的体现, java中没有哪种锁能够在所有情况下都能有出色的效率. 引入这么多锁的起因就是为了应对不同的情况.


网上也摘抄了不少博客上的内容, 自己整理了一下, 变成自己能看懂的.
参考来源:
Java synchronized原理总结
synchronized 底层原理
synchronized原理和锁优化策略(偏向/轻量级/重量级)

至此本章到这里就结束了, 看到这里, 假如对你有帮助, 请点赞关注. 谢谢大家.
说明
1. 本站所有资源来源于用户上传和网络,如有侵权请邮件联系站长!
2. 分享目的仅供大家学习和交流,您必须在下载后24小时内删除!
3. 不得使用于非法商业用途,不得违反国家法律。否则后果自负!
4. 本站提供的源码、模板、插件等等其他资源,都不包含技术服务请大家谅解!
5. 如有链接无法下载、失效或广告,请联系管理员处理!
6. 本站资源售价只是摆设,本站源码仅提供给会员学习使用!
7. 如遇到加密压缩包,请使用360解压,如遇到无法解压的请联系管理员
开心源码网 » 从 Synchronized 到锁的优化

发表回复