synchronized原理分析

Synchronized的实现原理分析 

由于synchronized的实现是在jvm层面,所以我们如果要看它的源码,需要从字节码入手。下面代码演示了synchronized作为锁的两种用法,我们观察一下这段代码生成的字节码

public class App {
    public synchronized void test1(){
    }
    public void test2(){
        synchronized (this){
        }
    }
    public static void main( String[] args ){
        System.out.println( "Hello World!" );
    }
}

进入classpath目录下找到App.class文件, 在cmd中输入 javap -v App.class查看字节码

public synchronized void test1();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=0, locals=1, args_size=1
         0: return
      LineNumberTable:
        line 10: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       1     0  this   Lcom/gupaoedu/openclass/App;

  public void test2();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: aload_0
         1: dup
         2: astore_1
         3: monitorenter  //监视器进入,获取锁
         4: aload_1
         5: monitorexit  //监视器退出,释放锁
         6: goto          14
         9: astore_2
        10: aload_1
        11: monitorexit
        12: aload_2
        13: athrow
        14: return

通过字节码我们可以发现,修饰在方法层面的同步关键字,会多一个 ACC_SYNCHRONIZED的flag;修饰在代码块层面的同步块会多一个 monitorenter和 monitorexit关键字。无论采用哪一种方式,本质上都是对一个对象的监视器(monitor)进行获取,而这个获取的过程是排他的,也就是同一个时刻只能有一个线程获得同步块对象的监视器。

synchronized关键字经过编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码指令。当我们的JVM把字节码加载到内存的时候,会对这两个指令进行解析。这两个字节码都需要一个Object类型的参数来指明要锁定和解锁的对象。如果Java程序中的synchronized明确指定了对象参数,那么这个对象就是加锁和解锁的对象;如果没有明确指定,那就根据synchronized修饰的是实例方法还是类方法,获取对应的对象实例或Class对象来作为锁对象

在分析monitor之前需要了解oop, oopDesc, markOop等相关概念

 

1.对象头:

synchronized实现的锁是存储在Java对象头里,什么是对象头呢?

在Hotspot虚拟机中,对象在内存中的存储布局,可以分为三个区域:对象头(Header)、实例数据(Instance Data)、对齐填充(Padding)

 

 2.instanceOopDesc & OopDesc

当我们在Java代码中,使用new创建一个对象实例的时候,(hotspot虚拟机)JVM层面实际上会创建一个 instanceOopDesc对象。

Hotspot虚拟机采用OOP-Klass模型来描述Java对象实例,OOP(Ordinary Object Point)指的是普通对象指针,Klass用来描述对象实例的具体类型。Hotspot采用instanceOopDesc和arrayOopDesc来描述对象头,arrayOopDesc对象用来描述数组类型

instanceOopDesc的定义在Hotspot源码中的 instanceOop.hpp文件中,另外,arrayOopDesc的定义对应 arrayOop.hpp

class instanceOopDesc : public oopDesc {
 public:
  // aligned header size.
  static int header_size() { return sizeof(instanceOopDesc)/HeapWordSize; }
  // If compressed, the offset of the fields of the instance may not be aligned.
  static int base_offset_in_bytes() {
    // offset computation code breaks if UseCompressedClassPointers
    // only is true
    return (UseCompressedOops && UseCompressedClassPointers) ?
             klass_gap_offset_in_bytes() :
             sizeof(instanceOopDesc);
  }
  static bool contains_field_offset(int offset, int nonstatic_field_size) {
    int base_in_bytes = base_offset_in_bytes();
    return (offset >= base_in_bytes &&
            (offset-base_in_bytes) < nonstatic_field_size * heapOopSize);
  }
};

 从instanceOopDesc代码中可以看到 instanceOopDesc继承自oopDesc,oopDesc的定义载Hotspot源码中的 oop.hpp文件中

class oopDesc {
  friend class VMStructs;
 private:
  volatile markOop  _mark;
  union _metadata {
    Klass*      _klass;
    narrowKlass _compressed_klass;
  } _metadata;
  // Fast access to barrier set.  Must be initialized.
  static BarrierSet* _bs;
  ...
}

在普通实例对象中,oopDesc的定义包含两个成员,分别是 _mark和 _metadata

  • _mark表示对象标记、属于markOop类型,也就是接下来要讲解的Mark World,它记录了对象和锁有关的信息
  • _metadata表示类元信息,类元信息存储的是对象指向它的类元数据(Klass)的首地址,其中Klass表示普通指针、 _compressed_klass表示压缩类指针

 

3.Mark Word

前面我们提到,普通对象的对象头由两部分组成,分别是markOop以及类元信息,markOop官方称为Mark Word 在Hotspot中,markOop的定义在 markOop.hpp文件中,代码如下

class markOopDesc: public oopDesc {
 private:
  // Conversion
  uintptr_t value() const { return (uintptr_t) this; }
 public:
  // Constants
  enum { age_bits                 = 4,  //分代年龄
         lock_bits                = 2, //锁标识
         biased_lock_bits         = 1, //是否为偏向锁
         max_hash_bits            = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
         hash_bits                = max_hash_bits > 31 ? 31 : max_hash_bits, //对象的hashcode
         cms_bits                 = LP64_ONLY(1) NOT_LP64(0),
         epoch_bits               = 2 //偏向锁的时间戳
  };
...

Mark word记录了对象和锁有关的信息,当某个对象被synchronized关键字当成同步锁时,那么围绕这个锁的一系列操作都和Mark word有关系。Mark Word在32位虚拟机的长度是32bit、在64位虚拟机的长度是64bit。 Mark Word里面存储的数据会随着锁标志位的变化而变化,Mark Word可能变化为存储以下5种情况

 

锁标志位的表示意义

  • 锁标识 lock=00 表示轻量级锁
  • 锁标识 lock=10 表示重量级锁
  • 偏向锁标识 biased_lock=1表示偏向锁
  • 偏向锁标识 biased_lock=0且锁标识=01表示无锁状态

 

4.monitor

前面提到了锁标志位的存储,但是为什么任意一个Java对象都能成为锁对象呢

  1. 首先,Java中的每个对象都派生自Object类,而每个Java Object在JVM内部都有一个native的C++对象 oop/oopDesc进行对应。
  2. 其次,线程在获取锁的时候,实际上就是获得一个监视器对象(monitor) ,monitor可以认为是一个同步对象,所有的Java对象是天生携带monitor.
  3. markOopDesc继承自oopDesc,并且扩展了自己的monitor方法,这个方法返回一个ObjectMonitor指针对象,在hotspot虚拟机中,采用ObjectMonitor类来实现monitor

在hotspot源码的 markOop.hpp文件中,可以看到下面这段代码。

bool has_monitor() const {
    return ((value() & monitor_value) != 0);
}
ObjectMonitor* monitor() const {
    assert(has_monitor(), "check");
    // Use xor instead of &~ to provide one extra tag-bit check.
    return (ObjectMonitor*) (value() ^ monitor_value);
}

ObjectMonitor.hpp中,可以看到ObjectMonitor的定义

class ObjectMonitor {
...
  ObjectMonitor() {
    _header       = NULL; //markOop对象头
    _count        = 0;    
    _waiters      = 0,   //等待线程数
    _recursions   = 0;   //重入次数
    _object       = NULL;  
    _owner        = NULL;  //获得ObjectMonitor对象的线程
    _WaitSet      = NULL;  //处于wait状态的线程,会被加入到waitSet
    _WaitSetLock  = 0 ; 
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁BLOCKED状态的线程
    _SpinFreq     = 0 ;   
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ; 
    _previous_owner_tid = 0; //监视器前一个拥有线程的ID
  }
...

多个线程访问同步代码块时,相当于去争抢对象监视器修改对象中的锁标识,上面的代码中ObjectMonitor这个对象和线程争抢锁的逻辑有密切的关系

 

简单总结一下,同步块的实现使用 monitorenter和 monitorexit指令,而同步方法是依靠方法修饰符上的flag ACC_SYNCHRONIZED来完成。其本质是对一个对象监视器(monitor)进行获取,这个获取过程是排他的,也就是同一个时刻只能有一个线程获得由synchronized所保护对象的监视器。所谓的监视器,实际上可以理解为一个同步工具,它是由Java对象进行描述的。在Hotspot中,是通过ObjectMonitor来实现,每个对象中都会内置一个ObjectMonitor对象

 

 

 

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值