本篇博客参考了死磕Synchronized底层实现--概论如果有兴趣了解更深的内容可以看看上面博客。
锁的状态
锁的状态总共有四种:无锁状态、偏向锁、轻量级锁和重量级锁。随着锁的竞争,锁可以从偏向锁升级到轻量级锁,再升级的重量级锁(但是锁的升级是单向的,也就是说只能从低到高升级,不会出现锁的降级)。锁的状态保存在对象头的Mark Word中,以32位的JDK为例:
(一)偏向锁
在实际应用运行过程中发现,“锁总是同一个线程持有,很少发生竞争”,也就是说锁总是被第一个占用它的线程拥有,这个线程就是锁的偏向线程。那么只需要在锁第一次被拥有的时候,记录下偏向线程ID。这样偏向线程就一直持有着锁。但只要有其他线程尝试获取锁,那么偏向锁就会升级为轻量级锁。
偏向锁获取的过程
1. 在当前线程的栈中创建一个锁记录(Lock Record)的空间,并将锁记录的obj属性指向锁对象。
2. 判断Mark Work中偏向锁的标记是否设置为1--确认为可偏向状态。
3. 如果锁对象是第一次被线程获取,JVM会将对象头的锁标志设置为“01”。
4. 然后使用CAS将Mark Word 中的线程ID设置为当前线程ID,如果设置成功,那么持有偏向锁的线程以后每次进入该同步代码块,就不用进行任何操作。
偏向锁的释放
偏向锁只有遇到其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁,线程不会主动去释放偏向锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有字节码正在执行),它会首先暂停拥有偏向锁的线程,判断锁对象是否处于被锁定状态(即当前线程是否在执行同步代码块),根据对象的状态,撤销偏向锁后恢复到未锁定(标志位为“01”)或轻量级锁(标志位为“00”)的状态。
(二)轻量级锁
“轻量级”是相对于使用操作系统互斥量来实现的传统锁而言的。但是,首先需要强调一点的是,轻量级锁并不是用来代替重量级锁的,它的本意是在没有多线程竞争的前提下,减少传统的重量级锁使用产生的性能消耗。轻量级锁所适应的场景是线程交替执行同步块的情况,如果存在多个线程同时尝试获取锁(即CAS获取锁失败)的情况,就会导致轻量级锁膨胀为重量级锁。
JVM8:
CASE(_monitorenter): {
oop lockee = STACK_OBJECT(-1);
...
if (entry != NULL) {
...
// 上面省略的代码中如果CAS操作失败也会调用到InterpreterRuntime::monitorenter
// traditional lightweight locking
if (!success) {
// 构建一个无锁状态的Displaced Mark Word
markOop displaced = lockee->mark()->set_unlocked();
// 设置到Lock Record中去
entry->lock()->set_displaced_header(displaced);
bool call_vm = UseHeavyMonitors;
//使用CAS将displaced替换到mark word中
//cmpxchg_ptr方法第一个参数是预期修改后的值,第2个参数是修改的对象,
//第3个参数是预期原值,方法返回实际原值,如果等于预期原值则说明修改成功。
//如果锁对象的mark word被替换为锁记录,则其他线程就无法CAS成功
if (call_vm || Atomic::cmpxchg_ptr(entry, lockee->mark_addr(), displaced) != displaced) {
// 如果CAS替换不成功,代表锁对象不是无锁状态,这时候判断下是不是锁重入
// Is it simple recursive case?
if (!call_vm && THREAD->is_lock_owned((address) displaced->clear_lock_bits())) {
entry->lock()->set_displaced_header(NULL);
} else {
// 有线程竞争锁则调用monitorenter
CALL_VM(InterpreterRuntime::monitorenter(THREAD, entry), handle_exception);
}
}
}
UPDATE_PC_AND_TOS_AND_CONTINUE(1, -1);
} else {
istate->set_msg(more_monitors);
UPDATE_PC_AND_RETURN(0); // Re-execute
}
}
轻量级锁的获取过程
1. 在进入同步代码块的时候,如果同步对象锁状态为无锁状态(锁标志为01,偏向锁标志为0),JVM会在当前线程的栈中创建一个锁记录(Lock Record)的空间(锁记录只作用于轻量级锁中,在偏向锁和重量级锁中只是起过渡作用),用于存储锁对象的Mark Word的拷贝---Displaced Mark Word,如果是处于偏向锁状态,则使用原来的锁记录。
2. 拷贝之后,当前线程使用CAS将锁对象的Mark Word更新为指向锁记录的指针,并将锁记录里的owner指针指向锁对象的mark word。
3. 如果成功更新,则表示当前线程成功获得了锁,并且对象Mark Word的锁标志为“00”,表示此对象处于轻量级锁状态。
4. 如果更新失败,JVM会检查对象的Mark Word是否指向当前线程的锁记录,如果是,则说明当前线程是锁重入,那么使用CAS把上面锁记录中的Displaced Mark Word设置为null,继续执行同步代码块。否则,说明有多个线程同时竞争锁,那么轻量级锁会膨胀为重量级锁。锁标志的状态值变为“10”。后面等待锁的线程要进入阻塞状态。 而当前线程便尝试使用自旋来获取锁,自旋就是为了不让线程阻塞,而采用循环去获取锁的过程。
锁重入中的栈与对象头如下:
轻量级锁的释放过程:
线程通过CAS将栈帧中的Displaced Mark Word 替换回锁对象的Mark Word中,如果替换成功,则释放锁。否则,说明有其他线程尝试获取锁(此时锁已经膨胀为重量级锁),那就要在释放锁的同时,唤醒被挂起的线程。
轻量级锁主要有两种
1. 自旋锁
所谓自旋,是指当有另一个线程想获取被其它线程持有的锁的时候,不会进入阻塞状态,而是使用空循环来进行自旋。注意:自旋是会消耗cpu的,所以,轻量级锁适用于那些同步代码块执行的很快的场景,这样,线程原地等待很短很短的时间就能够获得锁了。
自旋锁的一些问题
如果同步代码块执行的很慢,需要消耗大量的时间,那么这个时侯,其他线程在空循环,消耗cpu。
本来一个线程把锁释放之后,当前线程是能够获得锁的,但是假如这个时候有好几个线程都在竞争这个锁的话,那么有可能当前线程会获取不到锁,还得原地等待继续空循环消耗cup,甚至有可能一直获取不到锁。
基于这个问题,我们必须给线程空循环设置一个次数,当线程超过了这个次数,我们就认为,继续使用自旋锁就不适合了,此时锁会再次膨胀,升级为重量级锁。
默认情况下,自旋的次数为10次,用户可以通过-XX:PreBlockSpin来进行更改。
2.自适应自旋锁
所谓自适应自旋锁就是线程空循环等待的自旋次数并非是固定的,而是会动态着根据实际情况来改变自旋等待的次数。
延长自旋次数:假如一个线程1刚刚成功获得一个锁,当它把锁释放了之后,线程2获得该锁,并且线程2在运行的过程中,此时线程1又想来获得该锁了,但线程2还没有释放该锁,所以线程1只能自旋等待,但是虚拟机认为,由于线程1刚刚获得过该锁,那么虚拟机觉得线程1这次自旋也是很有可能能够再次成功获得该锁的,所以会延长线程1自旋的次数。
减少自旋次数:,如果对于某一个锁,一个线程自旋之后,很少成功获得该锁,那么以后这个线程获取该锁时,是有可能直接忽略掉自旋过程,直接升级为重量级锁的,以免空循环等待浪费资源。
(三)重量级锁
重量级锁是依赖对象内部的monitor锁来实现的,而monitor又依赖操作系统的MutexLock(互斥锁)来实现的,所以重量级锁也被成为互斥锁。
每一个JAVA对象都会与一个监视器monitor关联,我们可以把它理解成为一把锁,当一个线程想要执行同步代码块时,该线程得先获取到锁对象对应的monitor。当一个monitor被持有后,它将处于锁定状态。
JVM8:锁膨胀为重量级锁
for (;;) {
const markOop mark = object->mark() ;
assert (!mark->has_bias_pattern(), "invariant") ;
// CASE: inflated
if (mark->has_monitor()) {
// 已经是重量级锁状态了,直接返回
ObjectMonitor * inf = mark->monitor() ;
...
return inf ;
}
// CASE: inflation in progress
if (mark == markOopDesc::INFLATING()) {
// 正在膨胀中,说明另一个线程正在进行锁膨胀,continue重试
TEVENT (Inflate: spin while INFLATING) ;
// 在该方法中会进行spin/yield/park等操作完成自旋动作
ReadStableMark(object) ;
continue ;
}
if (mark->has_locker()) {
// 当前轻量级锁状态,先分配一个ObjectMonitor对象,并初始化值
ObjectMonitor * m = omAlloc (Self) ;
m->Recycle();
m->_Responsible = NULL ;
m->OwnerIsThread = 0 ;
m->_recursions = 0 ;
m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ; // Consider: maintain by type/class
// 将锁对象的mark word设置为INFLATING (0)状态
markOop cmp = (markOop) Atomic::cmpxchg_ptr (markOopDesc::INFLATING(), object->mark_addr(), mark) ;
if (cmp != mark) {
omRelease (Self, m, true) ;
continue ; // Interference -- just retry
}
// 栈中的displaced mark word
markOop dmw = mark->displaced_mark_helper() ;
assert (dmw->is_neutral(), "invariant") ;
// 设置monitor的字段
m->set_header(dmw) ;
// owner为Lock Record
m->set_owner(mark->locker());
m->set_object(object);
...
// 将锁对象头设置为重量级锁状态
object->release_set_mark(markOopDesc::encode(m));
...
return m ;
}
// CASE: neutral
// 分配以及初始化ObjectMonitor对象
ObjectMonitor * m = omAlloc (Self) ;
// prepare m for installation - set monitor to initial state
m->Recycle();
m->set_header(mark);
// owner为NULL
m->set_owner(NULL);
m->set_object(object);
...
// 用CAS替换对象头的mark word为重量级锁状态
if (Atomic::cmpxchg_ptr (markOopDesc::encode(m), object->mark_addr(), mark) != mark) {
// 不成功说明有另外一个线程在执行inflate,释放monitor对象
m->set_object (NULL) ;
m->set_owner (NULL) ;
m->OwnerIsThread = 0 ;
m->Recycle() ;
omRelease (Self, m, true) ;
m = NULL ;
continue ;
}
...
return m ;
}
重量级锁的获取过程:
在进入同步代码块的时候,如果对象锁处于轻量级锁(锁标志位为00),JVM会分配该对象一个monitor对象,并初始化。使用CAS将锁对象的Mark Word设置为INFLATING(膨胀中)状态,一旦设置成功,其他线程会自旋地等待膨胀完成。之后会将monitor对象的header属性设置为栈帧中的Displaced Mark Word,owner指针指向锁记录(Lock Record),obj属性指向锁对象。锁标志的状态值设置为“10”,mark word指向monitor对象。在将锁膨胀后,该线程会获取锁,并将monitor对象中的owner指针指向当前线程。
如果对象锁处于无锁状态(锁标志位为01),则不需要使用锁记录(Lock Record只用于轻量级锁中),会将monitor对象的header属性设置为对象锁的Mark Word,owner指针为当前线程,obj属性指向锁对象,锁标志的状态值设置为“10”,mark word指向monitor对象。
如果获取锁失败,则将该线程放入到lock pool中,然后挂起该线程(进入阻塞状态),直到锁被释放时,才有可能抢夺锁成功,进入就绪状态。
如果线程获得锁后调用wait方法,则会将线程加入到等待队列中,当被notify唤醒后,会将线程从等待队列移动到锁池中去。需要注意的是,当调用一个锁对象的wait或notify方法时,如当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁。
重量级锁的释放过程:
设置monitor对象的owner指针为null,即释放锁,并唤醒被阻塞的线程。
为什么说重量级锁开销大呢
主要是,当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。