1.对象头
每一个Java类,在被JVM加载的时候,JVM会给这个类创建一个instanceKlass
,保存在方法区,用来在JVM层表示该Java类。当我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个instanceOopDesc
对象,这个对象中包含了对象头以及实例数据。
Java对象保存在堆内存中。在内存中,一个Java对象包含三部分:对象头、实例数据和对齐填充。其中对象头是一个很关键的部分,因为对象头中包含锁状态标志、线程持有的锁等标志。
class oopDesc {
friend class VMStructs;
private:
volatile markOop _mark;
union _metadata {
wideKlassOop _klass;
narrowOop _compressed_klass;
} _metadata;
}
上面代码中的_mark
和_metadata
其实就是对象头的定义。
- _metadata是一个联合体,这个字段被称为元数据指针。指向描述类型Klass对象的指针(实例数据)。
_mark
,即mark word。对象头信息是与对象自身定义的数据无关的额外存储成本,考虑到虚拟机的空间效率,Mark Word被设计成一个非固定的数据结构以便在极小的空间内存储尽量多的信息,它会根据对象的状态复用自己的存储空间。对markword的设计方式上,非常像网络协议报文头:将mark word划分为多个比特位区间,并在不同的对象状态下赋予比特位不同的含义。
虚拟机位数 | 对象头结构 | 说明 |
32bit | Mark Word | 存储对象的hashcode、锁信息或分代年龄GC标志等信息 |
64bit | Class Metadate Address | 类型指针指向对象的元数据、JVM通过这个指针确定这个对象是那个类的实例 |
而 Mark Word 的结构(不固定)如下所示
锁状态 | 25bit | 4bit | 1bit | 2bit | |
23bit | 2bit | 是否偏向锁 | 锁标志位 | ||
无锁 | 对象的HashCode | 分代年龄 | 0 | 01 | |
偏向锁 | 线程ID | Epoch | 分代年龄 | 1 | 01 |
轻量级锁 | 指向栈中锁记录的指针 | 00 | |||
重量级锁 | 指向重量级锁的指针 | 10 | |||
GC标记 | 空 | 11 |
而 Class Metadate Address 的结构如下所示
锁状态 | 25bit | 32bit | 1bit | 4bit | 1bit | 2bit |
|
| cms_free | 分代年龄 | 是否偏向锁 | 锁标志位 | |
无锁 | unused | HashCode |
| 0 | 01 | |
偏向锁 | Epoch(2bit) 线程ID(54) |
| 1 | 01 |
对象头中主要包含了GC分代年龄、锁状态标记、哈希码、epoch等信息。
从上图中可以看出,对象的状态一共有五种,分别是无锁态、轻量级锁、重量级锁、GC标记和偏向锁。在32位的虚拟机中有两个Bits是用来存储锁的标记为的,但是我们都知道,两个bits最多只能表示四种状态:00、01、10、11,那么第五种状态如何表示呢 ,就要额外依赖1Bit的空间,使用0和1来区分。
2.对象头总结
每一个Java类,在被JVM加载的时候,JVM会给这个类创建一个instanceKlass,保存在方法区,用来在JVM层表示该Java类。当我们在Java代码中,使用new创建一个对象的时候,JVM会创建一个instanceOopDesc对象,这个对象中包含了两部分信息,方法头以及元数据。对象头中有一些运行时数据,其中就包括和多线程相关的锁的信息。元数据其实维护的是指针,指向的是对象所属的类的instanceKlass。
3.monitor
可以看到 Mark Word 包括重量级锁,重量级锁的指针指向 monitor 对象(监视器锁)所以每一个对象都与一个 monitor 关联,当一个 monitor 被某个线程持有后, 它便处于锁定状态。谈及monitor 来看一下monitord 的实现,monitor 是由ObjectMonitor实现的,其主要数据结构如下
OobjectMonitor() {
_header = NULL;
_count = e; //用来记录该线程获取锁的次数
_waiters = e,
_recursions = 0;//锁的重入次数
__object = NULL;
_owner = NULL;//指向持有ObjectMonitor对象的线程
_WaitSet = NULL;//存放处于wait状态的线程队列
_WaitsetLock = NULL;
_Responsible = NULL;
_succ = NULL;
_CXq = NULL;
_FreeNext = NULL;
_EntryList = NULL;//存放处于等待锁block状态的线程队列
_SpinFreq = 0;
_SpinClock = 0;
OwnerIsThread = 0;
}
当多个线程同时请求某个对象监视器时,对象监视器会设置几种状态用来区分请求的线程:
- Entry List:存放处于等待锁block状态的线程队列
- wait Set:存放处于wait状态的线程队列
- onDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为 OnDeck
- owner:指向持有ObjectMonitor对象的线程
- !owner:释放锁的线程
- recursions:锁的重入次数
- count:用来记录该线程获取锁的次数
当多个线程同时访问一段同步代码时,首先会进入_EntryList
队列中,当某个线程获取到对象的monitor后进入_Owner
区域并把monitor中的_owner
变量设置为当前线程,同时monitor中的计数器_count
加1。即获得对象锁。
若持有monitor的线程调用wait()
方法,将释放当前持有的monitor,_owner
变量恢复为null
,_count
自减1,同时该线程进入_WaitSet
集合中等待被唤醒。若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)。如下图所示
下面这个图反应状态的转换关系
ObjectMonitor类中提供了几个方法:
3.1 获得锁
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();
}
}
3.2 释放锁
void ATTR ObjectMonitor::exit(TRAPS) {
Thread * Self = THREAD ;
//如果当前线程不是Monitor的所有者
if (THREAD != _owner) {
if (THREAD->is_lock_owned((address) _owner)) { //
// Transmute _owner from a BasicLock pointer to a Thread address.
// We don't need to hold _mutex for this transition.
// Non-null to Non-null is safe as long as all readers can
// tolerate either flavor.
assert (_recursions == 0, "invariant") ;
_owner = THREAD ;
_recursions = 0 ;
OwnerIsThread = 1 ;
} else {
// NOTE: we need to handle unbalanced monitor enter/exit
// in native code by throwing an exception.
// TODO: Throw an IllegalMonitorStateException ?
TEVENT (Exit - Throw IMSX) ;
assert(false, "Non-balanced monitor enter/exit!");
if (false) {
THROW(vmSymbols::java_lang_IllegalMonitorStateException());
}
return;
}
}
// 如果_recursions次数不为0.自减
if (_recursions != 0) {
_recursions--; // this is simple recursive enter
TEVENT (Inflated exit - recursive) ;
return ;
}
//省略部分代码,根据不同的策略(由QMode指定),从cxq或EntryList中获取头节点,通过ObjectMonitor::ExitEpilog方法唤醒该节点封装的线程,唤醒操作最终由unpark完成。
4 总结
上面介绍的就是虚拟机中Moniter的的加锁以及解锁的原理。
sychronized
加锁的时候,会调用objectMonitor的enter
方法,解锁的时候会调用exit
方法。事实上,只有在JDK1.6之前,synchronized
的实现才会直接调用ObjectMonitor的enter
和exit
,这种锁被称之为重量级锁。为什么说这种方式操作锁很重呢?
- Java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统的帮忙,这就要从用户态转换到核心态,因此状态转换需要花费很多的处理器时间,对于代码简单的同步块(如被
synchronized
修饰的get
或set
方法)状态转换消耗的时间有可能比用户代码执行的时间还要长,所以说synchronized
是java语言中一个重量级的操纵。
所以,在JDK1.6中出现对锁进行了很多的优化,进而出现轻量级锁,偏向锁,锁消除,适应性自旋锁,锁粗化(自旋锁在1.4就有 只不过默认的是关闭的,jdk1.6是默认开启的),这些操作都是为了在线程之间更高效的共享数据 ,解决竞争问题。