Synchronized 底层实现原理

457 阅读4分钟

前言

相较于 Synchronized 使用方法的学习来说,学习 Synchronized 的实现原理是更深的一个层次,这部分知识的掌握难度要较高一些,尽管如此,这篇文章在一定程度上,提供了学习 Synchronized 原理所需要掌握的知识,希望读者朋友可以耐心的看下去,相信你看到文章一半以后会感觉到越来越接近最初的目标。

1. 实现 Synchronized 的基础

  • Java 对象头
  • Monitor

1.1 对象头的 Mark Word 的结构

Q : 为什么设计为非固定结构?
A : 由于对象头中的信息是与对象自身数据没有关系的额外存储成本,考虑到 Java 虚拟机的空间效率,Mark Word 被设计成一个非固定的数据结构以便存储更多有效的数据。具体地,它会根据对象状态来复用这块存储空间。

1.2 Monitor 对象锁

每个对象都天生自带了一把看不见的锁,即 Monitor 对象锁,它自身也是一个对象

实现方式有两种:

  • Monitor 对象与数据对象一起创建、一起销毁
  • 当线程获取 Monitor 对象锁时自动生成

ObjectMonitor:对象锁的实现

// http://hg.openjdk.java.net/jdk8u/jdk8u/hotspot/file/29ef249e9953/src/share/vm/runtime/objectMonitor.hpp
ObjectMonitor() {
    _header       = NULL;
    _count        = 0;      // 当前持有对象锁的线程数
    _waiters      = 0,      
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;   // 当前持有对象锁的线程
    _WaitSet      = NULL;   // 等待池
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;  // 锁池
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
    _previous_owner_tid = 0;

其中,锁池 _EntryList 和 等待池 _WaitSet 是用来保存 ObjectWaiter 对象的列表,ObjectWaiter 是线程的封装。

当多个线程同时访问一个对象的同步代码时,首先进入 _EntryList,当一个 _EntryList 中的线程获取到对象锁后就进入到同步区域,并把 ObjectMonitor 中的 _Owner 设置成当前线程,计数器 _Count++。若线程调用 wait 方法,将释放对象锁, _Owner 被恢复为 null, _Count--,该线程进入到 _WaitSet 中等待被唤醒;若当前线程执行完毕,也会释放对象锁,并复位对应变量的值。

2. Synchronized 的效率优化

早期版本中,Monitor 是基于操作系统的 Mutex Lock 实现的,操作系统实现的线程切换需要从用户态转换到核心态,开销较大。

Java6 引入了锁优化技术,使得 Synchronized 的性能得到了较大提升,锁优化技术如下:

  • 自适应自旋锁
  • 锁消除
  • 锁粗化
  • 轻量级锁
  • 偏向锁

2.1 自旋锁与自适应自旋锁

共享数据锁定时间短,对与阻塞线程所做的挂起和恢复线程操作并不值得。在这种情况下,可使这些线程不放弃 CPU 而自旋(忙等待)来等待锁的释放

缺点:若锁定时间较长,自旋的线程会白白消耗 CPU 资源

由于每个锁的锁定时间不同,也就是说并不是每个锁都适合线程以自旋的方式去等待锁,那么如何判断一个锁是否适合自旋呢?通过前一次获取锁的线程的自旋的时间以及该线程的状态决定,若上一个线程通过自旋的方式成功地获取了锁,那么 JVM 认为自旋获取锁的可能性较大,会自动增加等待时间,比如增加 50 次循环;相反,如果自旋很少成功地获取到锁的话,JVM 可能会省略自旋过程,以避免浪费处理器资源。

2.2 锁消除

JIT 编译时,对运行时上下文进行扫描,去除那些不可能存在多线程竞争的锁

2.3 锁粗化

原则上,我们会尽量减少同步块的作用范围,即只在共享变量的实际作用域中才进行同步,这样是为了减少一些操作的不必要同步,也可以让等待锁的线程尽早的拿到锁。

如果存在一连串操作要对同一对象反复进行加锁和解锁,即使没有线程竞争,频繁地进行互斥同步锁操作会产生不必要的性能损耗。

为了解决上述问题,我们通过扩大锁的范围,避免多次反复的加锁和解锁来实现,这被称为锁粗化。