点击上方“蓝字” 可以关注我哦!
在 JDK 1.6 中对锁的实现引入了大量的优化。
减少锁操作的开销。
在看下面的内容之间,希望大家对 Mark Word 有个大体的理解。Java 中一个对象在堆中的内存结构是这样的:
Mark Word 是这样的:
自旋锁的思想:
让一个线程在请求一个共享数据的锁时执行忙循环(自旋)一段时间,如果在这段时间内能获得锁,就可以避免进入阻塞状态。
自旋锁的缺点:
需要进行忙循环操作占用 CPU 时间,它只适用于共享数据的锁定状态很短的场景。
若锁被其他线程长时间占用,会带来许多性能上的开销。所以自旋的次数不再固定。由前一次在同一个锁上的自旋时间及锁的拥有者的状态来决定。
如果共享数据的锁定状态持续时间较短,切换线程不值得(会有上下文切换),可以利用自旋锁尝试一定的次数。
JIT 编译时,会去除不可能存在竞争的锁。通过 JIT 的逃逸分析来消除一些没有在当前同步块以外被其他线程共享的数据的锁的保护,通过逃逸分析在 TLAB 来分配对象,这样就不存在共享数据带来的线程安全问题。
减少不必要的紧连在一起的 lock,unlock 操作,将多个连续的锁扩展成一个范围更大的锁。
为了在无线程竞争的情况下避免在锁获取过程中执行不必要的 CAS 原子指令,因为 CAS 原子指令虽然相对于重量级锁来说开销比较小但还是存在非常可观的本地延迟(因为 CAS 的底层是利用 LOCK 指令 + cmpxchg 汇编指令来保证原子性的,LOCK 指令会锁总线,其他 CPU 的内存操作将会被阻塞,因为 CPU 架构如果是 CMU 的话,控制信号、数据信号等是通过共享总线传到内存控制器中)。减少同一线程获取锁的代价,省去了大量有关锁申请的操作。
如果一个线程获得了锁, 那么锁就进入偏向模式,此时 Mark Word 的结构也变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查 Mark Word 的锁标记位为偏向锁以及当前线程 Id 等于 Mark Word 的 ThreadId 即可,这样就省去了大量有关锁申请的操作。
这种锁实现的背后基于这样一种假设,即在真实的情况下我们程序中的大部分同步代码一般都处于无锁竞争状态(即单线程执行环境),在无锁竞争的情况下完全可以避免调用操作系统层面的重量级互斥锁(重量级锁的底层就是这样实现的),只需要依靠一条 CAS 原子指令就可以完成锁的获取及释放。当存在锁竞争的情况下,执行 CAS 指令失败的线程将调用操作系统互斥锁进入到阻塞状态,当锁被释放的时候被唤醒。
因为虚拟机线程栈帧中的 Displaced Mark Word 是最初的无锁状态时的数据结构,所以用它来替换对象头中的 Mark Word 就可以释放锁。如果锁已经膨胀为重量级,此时是不可以被替换的,所以替换失败,唤醒被挂起的线程。
其实就是对象头中的 Mark Word 数据结构改变的过程。
只需要判断 Mark Word 中的一些值是否正确就行。
只有一个线程访问同步块时,使用偏向锁。
需要执行 CAS 操作自旋来获取锁。
如果执行同步块的时间比较少,那么多个线程之间执行使用轻量级锁交替执行。
会发生上下文切换,CPU 状态从用户态转换为内核态执行操作系统提供的互斥锁,所以系统开销比较大,响应时间也比较缓慢。
如果执行同步块的时间比较长,那么多个线程之间刚开始使用轻量级锁,后面膨胀为重量级锁。(因为执行同步块的时间长,线程 CAS 自旋获得轻量级锁失败后就会锁膨胀)
PS: 最近一直在疯狂准备面试,所以更新的频率比较低,望大家可以理解。等拿到 offer 后会有大量好文写给大家。
参考书籍:《深入理解 Java 虚拟机》