Java线程切换的实质
- Java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统的帮忙。
- 这就要从用户态转换到和心态,且状态转换需要花费很多处理器时间
如下代码:
private Object lock = new Object();
private int value = 0;
public void setValue() {
synchronized (lock) {
value++;
}
}
- 上诉代码中value++ 因为被关键字synchronized修饰,所以会在各个线程间同步执行。
- 但是value++消耗的时间很有可能比线程状态转换消耗的时间还短。所以说synchronized是java语言中一个重量级的操作
1.synchronized实现原理
对象头和Monitor
对象头
- Java对象在内存中的布局分为3部分:对象头,实例数据,对齐填充。
- java代码中,使用new关键字创建一个对象时,jvm会在堆中创建一个instanceOopDesc对象,这个对象中包含了对象头以及实例数据
instanceOopDesc的基类为oopDesc类,结构如下:
class oopDesc {
friend class VMStructs;
private:
volatile markOop _mark;
union _metadata {
wideKlassOop _klass;
narrowOop _compressed_klass;
} _metadata;
- 其中 __mark 和 metadata 一起组成对象头。
- _metadata主要保存了类元数据。
- _mark 是 markOop类型数据,一般称为标记字段(Mark Word),其中主要存储了对象的hashCode,分代年龄,锁标志位,是否偏向锁等
32位Java虚拟机的Mark Word的默认存储结构如下:
1.32位jvm的Mark Word默认存储结构.png- 默认情况下,没有线程进行加锁操作,所以对象中的Mark Word处于无锁状态。
- 考虑到jvm的空间效率,Mark Word被设计成一个非固定的数据结构,以便存储更多的有效数据。
- 他会根据对象本身的状态复用自己的存储空间,如32位jvm下,除了Mark Word的默认存储结构外,还有如下可能存在的结构:
- 从图中可以看出,根据“锁标志位”以及“是否为偏向锁”,Java中的锁可以分为以下几种状态:
是否偏向锁 | 锁标志位 | 锁状态 |
---|---|---|
0 | 01 | 无锁 |
1 | 01 | 偏向锁 |
0 | 00 | 轻量级锁 |
0 | 10 | 重量级锁 |
0 | 11 | GC标记 |
- 在Java6之前,并没有轻量级锁和偏向锁,只有重量级锁,也就是说synchronized的对象锁,锁标志位为10。
- 当锁是重量级锁时,对象头中Mark Word会用30bit来指向一个“互斥量”,这个互斥量就是Monitor
Monitor
- Monitor可以理解为一个同步工具,也可以描述为一种同步机制。它是一个保存在对象头_mark中的一个对象
- 在markOop类中有一个方法monitor() 会创建一个ObjectMonitor对象,OjbectMonitor就是Java虚拟机中的Monitor的具体实现。
- 因此Java中每个对象都会有一个对应的ObjectMonitor对象,这也是Java中所有的Object都可以作为锁对象的原因
ObjectMonitor实现锁同步机制
ObjectMonitor结构:
ObjectMonitor(){
_header = NULL;
_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; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0;
_SpinClock = 0;
OwnerIsThread =0;
}
其中几个比较关键的属性:
_owner: //指向持有ObjectMonitor对象的线程
_WaitSet: //存放处于wait状态的线程队列
_EntryList://存放处于等待锁block状态的线程队列
_recursions://锁的重入次数
_count; //记录该线程获取锁的次数
-
解析
- 当多个线程同时访问一段同步代码时,线程首先会进入_EntryList队列中,当某个线程通过竞争获取到对象的monitor后,monitor会把 _owner变量设置为当前线程,同时monotor中的计数器 _count加1,即获得对象锁
- 若持有monitor的线程调用wait()方法,将释放当前持有的monitor,_owner变量恢复为null, _count自减1,同时该线程进入 _WaitSet集合中等待被唤醒。
- 若当前线程执行完毕也将释放monitor(锁)并复位变量的值,以便其他线程进入获取monitor(锁)
-
实例演示
比如有3个线程分别执行以下同步代码块:
private Object lock = new Object();
public void synchMethod() {
synchronized (lock) {
// do something
}
}
1.锁对象是lock对象,jvm中每个对象实例都有一个对应的oopDesc实例,oopDesc实例中的标记字段(mark word)中的_mark 属性总有一个对应的ObjectMonitor对象,其内部结构如下:
3.1.ObjectMonitor内部对象结构.png2.现在使用3个线程来执行上面的同步代码块。默认情况下,3个线程都会处于阻塞状态,会存放在ObjectMonitor中的EntrySet队列中,如下所示:
3.2.默认下线程处于阻塞状态,存放在EntrySet队列中.png3.假设线程2首先通过竞争获取到了锁对象,则ObjectMonitor中的Owner指针指向线程2,并将count自加1,结果如下:
3.3.线程2竞争到锁对象,ObjectMonitor中的Owner指针指向线程2.png4.上图中Owner指向线程2,表示线程2已经成功获取到锁(Monitor)对象,其他线程只能处于阻塞(blocking)状态。
- wait操作:
- 如果线程2在执行过程中调用wait操作,则线程2会释放锁(Monitor)对象,以便其他线程进入获取锁(Monitor)对象,Owner变量恢复为null,count做减1操作。
- 同时线程2会被添加到WaitSet集合中,进入等待(waiting)状态并等待被唤醒。结果如下:
5.因为线程2释放了锁,线程1和线程3可以再次通过竞争去获取锁(Monitor)对象,如果线程1通过竞争获取到锁,则重新将Owner指向线程1,如下:
3.5.阻塞状态下线程竞争后重新获取锁.png6.如果在线程1执行过程中调用了notify操作将线程2唤醒。
- 则当前处于WaitSet中的线程2会被重新添加到EntrySet集合中,并尝试重新获取竞争锁(Monitor)对象。
- 但是notify操作并不会使线程1释放锁,结果如下:
7.当线程1中的代码执行完毕以后,同样会自动释放锁,以便其他线程再次获取锁对象
重量级锁
- ObjectMonitor的同步机制使jvm对操作系统级别Mutex Lock(互斥锁)的管理过程,期间都会转入操作系统内核。
- 也就是说synochronized实现锁,在“重量级锁”状态下,当多个线程之间切换上下文时,还是一个比较重量级的操作。
网友评论