扫码关注微信公众号

回复“面试手册”,获取本站PDF版

回复“简历”,获取高质量简历模板

回复“加群”,加入程序员交流群

回复“电子书”,获取程序员类电子书

当前位置: Java > Java并发高频面试题 > 35.synchronized关键字的底层原理

这个问题也是面试比较高频的一个问题,也是比较难理解的,理解synchronized需要一定的Java虚拟机的知识。

在jdk1.6之前,synchronized被称为重量锁,在jdk1.6中,为了减少获得锁和释放锁带来的性能开销,引入了偏向锁和轻量级锁。下面先介绍jdk1.6之前的synchronized原理。

  • 对象头

在HotSpot虚拟机中,Java对象在内存中的布局大致可以分为三部分:对象头实例数据填充对齐。因为synchronized用的锁是存在对象头里的,这里我们需要重点了解对象头。如果对象头是数组类型,则对象头由Mark WordClass MetadataAddressArray length组成,如果对象头非数组类型,对象头则由Mark WordClass MetadataAddress组成。在32位虚拟机中,数组类型的Java对象头的组成如下表:

内容说明长度
Mark Word存储对象的hashCode、分代年龄和锁标记位32bit
Class MetadataAddress存储到对象类型数据的指针32bit
Array length数组的长度32bit
对象头

这里我们需要重点掌握的是Mark Word

在运行期间,Mark Word中存储的数据会随着锁标志位的变化而变化,在32位虚拟机中,不同状态下的组成如下:

mark word
mark word

其中线程ID表示持有偏向锁线程的ID,Epoch表示偏向锁的时间戳,偏向锁和轻量级锁是在jdk1.6中引入的。

  • 重量级锁的底部实现原理:Monitor

在jdk1.6之前,synchronized只能实现重量级锁,Java虚拟机是基于Monitor对象来实现重量级锁的,所以首先来了解下Monitor,在Hotspot虚拟机中,Monitor是由ObjectMonitor实现的,其源码是用C++语言编写的,首先我们先下载Hotspot的源码,源码下载链接:http://hg.openjdk.java.net/jdk8/jdk8/hotspot,找到ObjectMonitor.hpp文件,路径是src/share/vm/runtime/objectMonitor.hpp,这里只是简单介绍下其数据结构

ObjectMonitor() {
    _header       = NULL;
    _count        = 0; //锁的计数器,获取锁时count数值加1,释放锁时count值减1,直到
    _waiters      = 0, //等待线程数
    _recursions   = 0; //锁的重入次数
    _object       = NULL; 
    _owner        = NULL; //指向持有ObjectMonitor对象的线程地址
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ; //阻塞在EntryList上的单向线程列表
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

其中 _owner、_WaitSet和_EntryList 字段比较重要,它们之间的转换关系如下图

monitor
monitor

从上图可以总结获取Monitor和释放Monitor的流程如下:

  1. 当多个线程同时访问同步代码块时,首先会进入到EntryList中,然后通过CAS的方式尝试将Monitor中的owner字段设置为当前线程,同时count加1,若发现之前的owner的值就是指向当前线程的,recursions也需要加1。如果CAS尝试获取锁失败,则进入到EntryList中。
  2. 当获取锁的线程调用wait()方法,则会将owner设置为null,同时count减1,recursions减1,当前线程加入到WaitSet中,等待被唤醒。
  3. 当前线程执行完同步代码块时,则会释放锁,count减1,recursions减1。当recursions的值为0时,说明线程已经释放了锁。

之前提到过一个常见面试题,为什么wait()notify()等方法要在同步方法或同步代码块中来执行呢,这里就能找到原因,是因为wait()notify()方法需要借助ObjectMonitor对象内部方法来完成。

  • synchronized作用于同步代码块的实现原理

前面已经了解Monitor的实现细节,而Java虚拟机则是通过进入和退出Monitor对象来实现方法同步和代码块同步的。这里为了更方便看程序字节码执行指令,我先在IDEA中安装了一个jclasslib Bytecode viewer插件。我们先来看这个synchronized作用于同步代码块的代码。

    public void run() {
        //其他操作.......
        synchronized (this){   //this表示当前对象实例,这里还可以使用syncTest.class,表示class对象锁
            for (int j = 0; j < 10000; j++) {
                i++;
            }
        }

    }

查看代码字节码指令如下:

 1 dup
 2 astore_1
 3 monitorenter     //进入同步代码块的指令
 4 iconst_0
 5 istore_2
 6 iload_2
 7 sipush 10000
10 if_icmpge 27 (+17)
13 getstatic #2 <com/company/syncTest.i>
16 iconst_1
17 iadd
18 putstatic #2 <com/company/syncTest.i>
21 iinc 2 by 1
24 goto 6 (-18)
27 aload_1
28 monitorexit     //结束同步代码块的指令
29 goto 37 (+8)
32 astore_3
33 aload_1
34 monitorexit     //遇到异常时执行的指令
35 aload_3
36 athrow
37 return

从上述字节码中可以看到同步代码块的实现是由monitorenter 和 monitorexit 指令完成的,其中monitorenter指令所在的位置是同步代码块开始的位置,第一个monitorexit 指令是用于正常结束同步代码块的指令,第二个monitorexit 指令是用于异常结束时所执行的释放Monitor指令。

  • synchronized作用于同步方法原理
    private synchronized void add() {
        i++;
    }

查看字节码如下:

0 getstatic #2 <com/company/syncTest.i>
3 iconst_1
4 iadd
5 putstatic #2 <com/company/syncTest.i>
8 return

发现这个没有monitorenter 和 monitorexit 这两个指令了,而在查看该方法的class文件的结构信息时发现了Access flags后边的synchronized标识,该标识表明了该方法是一个同步方法。Java虚拟机通过该标识可以来辨别一个方法是否为同步方法,如果有该标识,线程将持有Monitor,在执行方法,最后释放Monitor。

原理大概就是这样,最后总结一下,面试中应该简洁地如何回答synchroized的底层原理这个问题。

答:Java虚拟机是通过进入和退出Monitor对象来实现代码块同步和方法同步的,代码块同步使用的是monitorenter 和 monitorexit 指令实现的,而方法同步是通过Access flags后面的标识来确定该方法是否为同步方法。


点击面试手册,获取本站面试手册PDF完整版