Synchronized的底层原理解析

1. java线程是如何创建的

先看下Thread.java中的start方法

public synchronized void start() {
        ..................................
        nativeCreate(this, stackSize, daemon);
        ..................................
    }

nativeCreate显然,是通过jni调用通过c++实现的

在java_lang_Thread.cc中可以看到nativeCreate函数的注册,对应的native函数如下

<pre>static void Thread_nativeCreate(JNIEnv* env, jclass, jobject java_thread, jlong stack_size,
jboolean daemon) {
// There are sections in the zygote that forbid thread creation.
Runtime* runtime = Runtime::Current();
if (runtime->IsZygote() && runtime->IsZygoteNoThreadSection()) {
jclass internal_error = env->FindClass("java/lang/InternalError");
CHECK(internal_error != nullptr);
env->ThrowNew(internal_error, "Cannot create threads in zygote");
return;
}

Thread::CreateNativeThread(env, java_thread, stack_size, daemon == JNI_TRUE);
</pre>

对应的调用了Thread中的CreateNativeThread函数

<pre>void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon) {

..................................................
pthread_create_result = pthread_create(&new_pthread,
                                       &attr,
                                       Thread::CreateCallback,
                                       child_thread);

.....................................................

</pre>

看到最后,其实java线程对应的就是一个c++创建的线程,在这个c++的线程中,会不断的取出java的指令,然后解析执行

2. Synchronized的实现原理

首先尝试下对synchronized的代码进行反编译

public class SynchronizedTest {

    public synchronized void doSth(){
        System.out.println("Hello World");
    }

    public void doSth1(){
        synchronized (SynchronizedTest.class){
            System.out.println("Hello World");
        }
    }

执行反编译后

 public synchronized void doSth();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String Hello World
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return

  public void doSth1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #5                  // class com/hollis/SynchronizedTest
         2: dup
         3: astore_1
         4: monitorenter
         5: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         8: ldc           #3                  // String Hello World
        10: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        13: aload_1
        14: monitorexit
        15: goto          23
        18: astore_2
        19: aload_1
        20: monitorexit
        21: aload_2
        22: athrow
        23: return

对于同步方法,是使用ACC_SYNCHRONIZED进行实现的,对于同步的代码块是使用monitorenter和monitorexit进行实现的,ACC_SYNCHRONIZED其实也是调用了moniterenter和monitorexit

问题:

为什么有两个monitorexit,程序在执行中可能会发生crash,第二个monitorexit是在发生异常时候执行的,防止发生死锁

1. java对象在内存中的模型

无论是对方法还是对代码块家synchronized其实都是对一个对象进行了加锁,如果要了解具体的实现,需要先看一下java对象在内存中的模型

Java对象保存在堆内存中。在内存中,一个Java对象包含三部分:对象头、实例数据和对齐填充。其中对象头是一个很关键的部分,因为对象头中包含锁状态标志、线程持有的锁等标志

HotSpot是基于c++实现,而c++是一门面向对象的语言,本身是具备面向对象基本特征的,所以Java中的对象表示,最简单的做法是为每个Java类生成一个c++类与之对应。但HotSpot JVM并没有这么做,而是设计了一个OOP-Klass Model。OOP(Ordinary Object Pointer)指的是普通对象指针,而Klass用来描述对象实例的具体类型。

简而言之oop指的是对象,kclass指的是class

1. oop体系

//定义了oops共同基类
typedef class   oopDesc*                            oop;
//表示一个Java类型实例
typedef class   instanceOopDesc*            instanceOop;
//表示一个Java方法
typedef class   methodOopDesc*                    methodOop;
//表示一个Java方法中的不变信息
typedef class   constMethodOopDesc*            constMethodOop;
//记录性能信息的数据结构
typedef class   methodDataOopDesc*            methodDataOop;
//定义了数组OOPS的抽象基类
typedef class   arrayOopDesc*                    arrayOop;
//表示持有一个OOPS数组
typedef class   objArrayOopDesc*            objArrayOop;
//表示容纳基本类型的数组
typedef class   typeArrayOopDesc*            typeArrayOop;
//表示在Class文件中描述的常量池
typedef class   constantPoolOopDesc*            constantPoolOop;
//常量池告诉缓存
typedef class   constantPoolCacheOopDesc*   constantPoolCacheOop;
//描述一个与Java类对等的C++类
typedef class   klassOopDesc*                    klassOop;
//表示对象头
typedef class   markOopDesc*                    markOop;

上面列出的是整个Oops模块的组成结构,其中包含多个子模块。每一个子模块对应一个类型,每一个类型的OOP都代表一个在JVM内部使用的特定对象的类型。

oopDesc有多个子类,oopDesc是其父类,对应的继承关系如下:

image

其中需要关注下oopDesc,这个跟synchronized的加锁有关

class oopDesc {
  friend class VMStructs;
  private:
      volatile markOop  _mark;
      union _metadata {
        wideKlassOop    _klass;
        narrowOop       _compressed_klass;
      } _metadata;

  private:
      // field addresses in oop
      void*     field_base(int offset)        const;

      jbyte*    byte_field_addr(int offset)   const;
      jchar*    char_field_addr(int offset)   const;
      jboolean* bool_field_addr(int offset)   const;
      jint*     int_field_addr(int offset)    const;
      jshort*   short_field_addr(int offset)  const;
      jlong*    long_field_addr(int offset)   const;
      jfloat*   float_field_addr(int offset)  const;
      jdouble*  double_field_addr(int offset) const;
      address*  address_field_addr(int offset) const;
}

重点关注下volatile markOop _mark,为什么要使用volatile呢,很明显这个跟加锁(synchronized)有关系

2. kclass体系

//klassOop的一部分,用来描述语言层的类型
class  Klass;
//在虚拟机层面描述一个Java类
class   instanceKlass;
//专有instantKlass,表示java.lang.Class的Klass
class     instanceMirrorKlass;
//专有instantKlass,表示java.lang.ref.Reference的子类的Klass
class     instanceRefKlass;
//表示methodOop的Klass
class   methodKlass;
//表示constMethodOop的Klass
class   constMethodKlass;
//表示methodDataOop的Klass
class   methodDataKlass;
//最为klass链的端点,klassKlass的Klass就是它自身
class   klassKlass;
//表示instanceKlass的Klass
class     instanceKlassKlass;
//表示arrayKlass的Klass
class     arrayKlassKlass;
//表示objArrayKlass的Klass
class       objArrayKlassKlass;
//表示typeArrayKlass的Klass
class       typeArrayKlassKlass;
//表示array类型的抽象基类
class   arrayKlass;
//表示objArrayOop的Klass
class     objArrayKlass;
//表示typeArrayOop的Klass
class     typeArrayKlass;
//表示constantPoolOop的Klass
class   constantPoolKlass;
//表示constantPoolCacheOop的Klass
class   constantPoolCacheKlass;

Kclass是所有class的父类

image

重新回到oopDesc中,里面有一个metadata的字段,如何将java对象和具体的class类型对应起来呢,就在这个metadata中

      union _metadata {
        wideKlassOop    _klass;
        narrowOop       _compressed_klass;
      } _metadata;

metadata中有_kclass和_compressed_kclass两个指针,他们都指向instanceKClass,那么instanceKClass是什么呢?看下具体的数据结构如下:

 //类拥有的方法列表
  objArrayOop     _methods;
  //描述方法顺序
  typeArrayOop    _method_ordering;
  //实现的接口
  objArrayOop     _local_interfaces;
  //继承的接口
  objArrayOop     _transitive_interfaces;
  //域
  typeArrayOop    _fields;
  //常量
  constantPoolOop _constants;
  //类加载器
  oop             _class_loader;
  //protected域
  oop             _protection_domain;

很显然,instanceKClass是描述类的信息,也就是说你这个类中有哪些函数,继承关系,已经常量等等。

oop和kclass的模型可以用下图描述

image
  1. mark和加锁

synchronized的加锁和_mark是密切相关的,_mark也叫做mark word

在32位的jvm上,结构如下

image

这个_mark主要记录了锁的信息,还有gc的相关信息

无论是基于synchronize的方法块或者是代码块,其实synchronize都指定了一个对象,而实际加锁的对象是这个对象对应的oop中mark word(_mark)

举个例子说明一下

Object object = new Object();
synchronized(object){
  System.out.println("test");
}

Object创建后,会在内存中生成一个oopDesc,在oopDesc中对应一个_mark,也就是说对这个_mark作为参照执行对应的加锁工作

  1. synchronized中的锁优化

偏向锁是JDK6中的重要引进,因为HotSpot作者经过研究实践发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低,引进了偏向锁。

偏向锁是在单线程执行代码块时使用的机制,如果在多线程并发的环境下(即线程A尚未执行完同步代码块,线程B发起了申请锁的申请),则一定会转化为轻量级锁或者重量级锁。

那么偏向锁是如何来减少不必要的CAS操作呢?

现在几乎所有的锁都是可重入的,即已经获得锁的线程可以多次锁住/解锁监视对象,按照之前的HotSpot设计,每次加锁/解锁都会涉及到一些CAS操作(比如对等待队列的CAS操作),CAS操作会延迟本地调用,因此偏向锁的想法是 一旦线程第一次获得了监视对象,之后让监视对象“偏向”这个线程,之后的多次调用则可以避免CAS操作,说白了就是置个变量,如果发现为true则无需再走各种加锁/解锁流程。

具体对应到实现是在_mark 中存储一个线程的id,每当有线程要执行加锁,先查看下这个线程id是否等于当前线程的id,如果相等,就不需要执行CAS操作,而是直接往下执行代码

  1. CAS实现

CAS全称compare-and-swap,其作用是让CPU比较后原子地更新某个位置的值,经过调查发现,其实现方式是基于硬件平台的汇编指令,就是说CAS是靠硬件实现的,JVM只是封装了汇编调用,那些AtomicInteger类便是使用了这些封装后的接口。

  1. 偏向锁的具体加锁流程

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程进入和退出同步块时不需要花费CAS操作来争夺锁资源,只需要检查是否为偏向锁、锁标识为以及ThreadID即可,处理流程如下:

1)检测Mark Word是否为可偏向状态,即是否为偏向锁1,锁标识位为01;

2)若为可偏向状态,则测试线程ID是否为当前线程ID,如果是,则执行步骤(5),否则执行步骤(3);

3)如果测试线程ID不为当前线程ID,则通过CAS操作竞争锁,竞争成功,则将Mark Word的线程ID替换为当前线程ID,否则执行线程(4);

4)通过CAS竞争锁失败,证明当前存在多线程竞争情况,当到达全局安全点,获得偏向锁的线程被挂起,偏向锁升级为轻量级锁,然后被阻塞在安全点的线程继续往下执行同步代码块;

5)执行同步代码块;

  1. 轻量锁

偏向锁是针对只有一个线程,并未发生资源竞争时候使用的一种锁的机制,对应到jvm中也就是之前讲的CAS操作,其执行步骤如下

  1. 在线程进入同步块时,检查_mark中的锁状态标志位,如果同步对象锁状态为无锁状态(锁标志位为“01”状态,是否为偏向锁为“0”),虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word。
  2. 拷贝对象头中的Mark Word复制到锁记录(Lock Record)中;
  3. 拷贝成功后,虚拟机将使用CAS操作尝试将对象Mark Word中的Lock Word更新为指向当前线程Lock Record的指针,并将Lock record里的owner指针指向object mark word。如果更新成功,则执行步骤(4),否则执行步骤(5);
  4. 如果这个更新动作成功了,那么当前线程就拥有了该对象的锁,并且对象Mark Word的锁标志位设置为“00”,即表示此对象处于轻量级锁定状态,此时线程堆栈与对象头的状态如下图所示:
  5. 如果这个更新操作失败了,虚拟机首先会检查对象Mark Word中的Lock Word是否指向当前线程的栈帧,如果是,就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行。否则说明多个线程竞争锁,进入自旋执行(3),若自旋结束时仍未获得锁,轻量级锁就要膨胀为重量级锁,锁标志的状态值变为“10”,Mark Word中存储的就是指向重量级锁(互斥量)的指针,当前线程以及后面等待锁的线程也要进入阻塞状态。
  6. 重量级锁

Synchronized在进入同步代码前,会先使用偏向锁,失败了转换成轻量锁(自旋锁),自旋一定的次数失败后就会升级为重量锁,重量锁是通过对象内部的一个叫做 监视器锁(Monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统Mutex Lock所实现的锁我们称之为 “重量级锁”。

  1. Monitor具体代码实现

前面提到的monitorenter,monitorexit其实是基于Monitor实现的,具体对应的类是ObjectMonitor

  ObjectMonitor() {
    _header       = NULL;
    _count        = 0;   
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;   //正在执行的线程id
    _WaitSet      = NULL;
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ;
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }

还提供了enter方法,其实就是对应monitorenter这个关键字

void ATTR ObjectMonitor::enter(TRAPS) {
  Thread * const Self = THREAD ;
  void * cur ;
  //通过CAS尝试把monitor的`_owner`字段设置为当前线程
  cur = Atomic::cmpxchg_ptr (Self, &_owner, NULL) ;
  //获取锁失败
  if (cur == NULL) {         assert (_recursions == 0   , "invariant") ;
     assert (_owner      == Self, "invariant") ;
     // CONSIDER: set or assert OwnerIsThread == 1
     return ;
  }
  // 如果旧值和当前线程一样,说明当前线程已经持有锁,此次为重入,_recursions自增,并获得锁。
  if (cur == Self) { 
     // TODO-FIXME: check for integer overflow!  BUGID 6557169.
     _recursions ++ ;
     return ;
  }

  // 如果当前线程是第一次进入该monitor,设置_recursions为1,_owner为当前线程
  if (Self->is_lock_owned ((address)cur)) { 
    assert (_recursions == 0, "internal state error");
    _recursions = 1 ;
    // Commute owner from a thread-specific on-stack BasicLockObject address to
    // a full-fledged "Thread *".
    _owner = Self ;
    OwnerIsThread = 1 ;
    return ;
  }

  // 省略部分代码。
  // 通过自旋执行ObjectMonitor::EnterI方法等待锁的释放
  for (;;) {
  jt->set_suspend_equivalent();
  // cleared by handle_special_suspend_equivalent_condition()
  // or java_suspend_self()

  EnterI (THREAD) ;

  if (!ExitSuspendEquivalent(jt)) break ;

  //
  // We have acquired the contended monitor, but while we were
  // waiting another thread suspended us. We don't want to enter
  // the monitor while suspended because that would surprise the
  // thread that suspended us.
  //
      _recursions = 0 ;
  _succ = NULL ;
  exit (Self) ;

  jt->java_suspend_self()

从代码中可以看到moniterenter也是先执行偏向锁,如果偏向锁不满足,升级成自旋锁,自旋超过一定次数就会升级成重量锁

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

友情链接更多精彩内容