所有的Java 对象都有自己唯一的隐式同步锁。该锁只能同时被一个线程获得,其他试图获得该锁的线程都会被阻塞在对象的等待队列中直到获得该锁的线程释放锁才能继续工作。
synchronized锁的几种状态
jdk1.6及以前,synchronized是重量级锁(需要靠操作系统阻塞和唤醒,因此效率不高)
锁升级过程
jdk1.6之后,synchronized锁分为4种状态
- 原本是“无锁”状态
- 一个线程加锁,将线程id写入对象头markwork,升级为“偏向锁”
- 多个线程cas加锁,轻量竞争,升级为“轻量级锁”
- cas自旋10次(jdk参数-XX:BiasedLockingStartupDelay=10可调)不成功(锁膨胀),重度竞争,升级为”重量级锁“
重量级锁通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。
synchronized会修改对象头中的markword,具体参考:
偏向锁、轻量级锁和重量级锁对比:
锁类型 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁、解锁不需要额外资源消耗,效率较高 | 如果线程间存在锁竞争,会带来额外的解锁消耗 | 适用只有一个线程访问同步块的情景 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序响应速度 | 如果获取锁失败,会进入自旋消耗cpu | 针对锁占用时间短,对响应时间比较敏感的情况 |
重量级锁 | 线程竞争不使用自旋,不消耗cpu | 线程会被阻塞,影响响应时间 | 锁占用时间较长,对吞吐量要求较高 |
锁消除
消除锁是虚拟机另外一种锁的优化,Java虚拟机在JIT编译时(可以简单理解为当某段代码即将第一次被执行时进行编译,又称即时编译),通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过这种方式消除没有必要的锁,可以节省毫无意义的请求锁时间。
锁粗化
我们都知道在加同步锁时,尽可能的将同步块的作用范围限制到尽量小的范围,但是如果存在连串的一系列操作都对同一个对象反复加锁和解锁,甚至加锁操作时出现在循环体中的,那即使没有线程竞争,频繁的进行互斥同步操作也会导致不必要的性能操作,所以Java虚拟机会增大锁的范围,例如放到循环体之外。
synchronized释放锁的情况:
1:占有锁的线程执⾏完了该代码块,然后释放对锁的占有;
2:占有锁线程执⾏发⽣异常,此时JVM会让线程⾃动释放锁;
3:占有锁线程进⼊ WAITING 状态从⽽释放锁,例如在该线程中调⽤wait()⽅法
synchronized关键字原理
Java对象头和monitor是实现synchronized的基础。
Monitor对象存在于每个Java对象的对象头Mark Word中(存储的指针的指向),Synchronized锁便是通过这种方式获取锁的,也是为什么Java中任意对象可以作为锁的原因,同时notify/notifyAll/wait等方法会使用到Monitor锁对象,所以必须在同步代码块中使用。
ObjectMonitor C++源码定义
ObjectMonitor中有两个队列,_WaitSet 和 _EntryList,用来保存ObjectWaiter对象列表( 每个等待锁的线程都会被封装成ObjectWaiter对象 )。
_owner指向持有ObjectMonitor对象的线程,当多个线程同时访问一段同步代码时:
1、首先会进入 _EntryList 集合,当线程获取到对象的monitor后,进入 _Owner区域并把monitor中的owner变量设置为当前线程,同时monitor中的计数器count加1;
2、若线程调用 wait() 方法,将释放当前持有的monitor,owner变量恢复为null,count自减1,同时该线程进入 WaitSet集合中等待被唤醒;
3、若当前线程执行完毕,也将释放monitor(锁)并复位count的值,以便其他线程进入获取monitor(锁);
当多个线程同时请求某个对象锁时,对象锁会设置⼏种状态⽤来区分请求的线程:
Contention List:所有请求锁的线程将被⾸先放置到该竞争队列
Entry List:Contention List中那些有资格成为候选⼈的线程被移到Entry List
Wait Set:那些调⽤wait⽅法被阻塞的线程被放置到Wait Set
OnDeck:任何时刻最多只能有⼀个线程正在竞争锁,该线程称为OnDeck
Owner:获得锁的线程称为Owner
!Owner:释放锁的线程
当⼀个线程尝试获得锁时,如果该锁已经被占⽤,则会将该线程封装成⼀个 ObjectWaiter 对象插⼊到Contention List的队列的队⾸,然后调⽤ park 函数挂起当前线程。
synchronized加解锁实现过程
synchronized关键字编译之后,会在同步块的前后分别形成monitorenter和monitorexit这两个字节码
synchronized关键字基于上述两个指令实现了锁的获取和释放过程。
1、加锁过程
解释器执行monitorenter时会进入到 InterpreterRuntime::monitorenter函数,具体C++代码实现如下:
如果开启偏向锁,进入fast_enter方法,否则直接执行slow_enter方法升级为轻量或重量级锁
slow_enter方法,会执行inflate方法进行锁的膨胀升级,返回ObjectMonitor后,执行enter方法
ObjectMonitor * ATTR ObjectSynchronizer::inflate (Thread * Self, oop object) {
//开始自旋转操作
for (;;) {
const markOop mark = object->mark() ;
//1.如果当前锁已经为重量级锁了,直接返回
if (mark->has_monitor()) {
ObjectMonitor * inf = mark->monitor() ;
return inf ;
}
//2.如果正在膨胀的过程中,在完成膨胀过程中,其他线程必须等待
if (mark == markOopDesc::INFLATING()) {
ReadStableMark(object) ;
continue ;
}
//3.如果当前为轻量级锁,迫使其膨胀为重量级锁
if (mark->has_locker()) {
//构造一个monitor对象
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
//先加个膨胀标志,紧紧是个标志 因为从轻量级到重量级需要一个复杂的过程
markOop cmp = (markOop) Atomic::cmpxchg_ptr (markOopDesc::INFLATING(), object->mark_addr(), mark) ;
if (cmp != mark) {
omRelease (Self, m, true) ;
continue ; // Interference -- just retry
}
//获取到当前的lock 标志
markOop dmw = mark->displaced_mark_helper() ;
//将它放入到moitor 头节点中来
m->set_header(dmw) ;
//设置锁的owner
m->set_owner(mark->locker());
//设置锁的对象
m->set_object(object);
//将新的monitor对象就设置到对象头中
object->release_set_mark(markOopDesc::encode(m));
if (ObjectMonitor::_sync_Inflations != NULL) ObjectMonitor::_sync_Inflations->inc() ;
TEVENT(Inflate: overwrite stacklock) ;
if (TraceMonitorInflation) {
if (object->is_instance()) {
ResourceMark rm;
tty->print_cr("Inflating object " INTPTR_FORMAT " , mark " INTPTR_FORMAT " , type %s",
(void *) object, (intptr_t) object->mark(),
object->klass()->external_name());
}
}
//直接返回新的锁对象
return m ;
}
//对象是空闲的时候 这就是踏空事件 代码跑到这时候 另一个线程刚刚释放了锁
ObjectMonitor * m = omAlloc (Self) ;
// prepare m for installation - set monitor to initial state
m->Recycle();
m->set_header(mark);
m->set_owner(NULL);
m->set_object(object);
m->OwnerIsThread = 1 ;
m->_recursions = 0 ;
m->_Responsible = NULL ;
m->_SpinDuration = ObjectMonitor::Knob_SpinLimit ; // consider: keep metastats by type/class
//将monitor 放置到对象的mark word中
if (Atomic::cmpxchg_ptr (markOopDesc::encode(m), object->mark_addr(), mark) != mark) {
m->set_object (NULL) ;
m->set_owner (NULL) ;
m->OwnerIsThread = 0 ;
m->Recycle() ;
omRelease (Self, m, true) ;
m = NULL ;
continue ;
}
return m ;
}
}
enter方法
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") ;
return ;
}
//如果之前的_owner指向该THREAD,那么该线程是重⼊,_recursions++
if (cur == Self) {
_recursions ++ ;
return ;
}
//如果当前线程是第⼀次进⼊该monitor,设置_recursions为1,_owner为当前线程
if (Self->is_lock_owned ((address)cur)) {
assert (_recursions == 0, "internal state error");
_recursions = 1 ; //_recursions标记为1
_owner = Self ; //设置owner
OwnerIsThread = 1 ;
return ;
}
……
总结:
1. 如果monitor的进⼊数为0,则该线程进⼊monitor,然后将进⼊数设置为1,该线程即为monitor的owner
2. 如果线程已经占有该monitor,只是重新进⼊,则进⼊monitor的进⼊数加1.
3. 如果其他线程已经占⽤了monitor,则该线程进⼊阻塞状态,直到monitor的进⼊数为0,再重新尝试获取monitor的所有权
2、解锁过程
解释器执行monitorexit时会进入到 InterpreterRuntime::monitorexit函数,具体C++代码实现如下:
当线程释放锁时,会从Contention List或EntryList中挑选⼀个线程唤醒,被选中的线程叫做 Heir presumptive 即假定继承⼈,假定继承⼈被唤醒后会尝试获得锁,但 synchronized 是⾮公平的,所以假定继承⼈不⼀定能获得锁。这是因为对于重量级锁,线程先⾃旋尝试获得锁,这样做的⽬的是为了减少执⾏操作系统同步操作带来的开销。如果⾃旋不成功再进⼊等待队列。这对那些已经在等待队列中的线程来说,稍微显得不公平,还有⼀个不公平的地⽅是⾃旋线程可能会抢占了Ready线程的锁。线程获得锁后调⽤ Object.wait ⽅法,则会将线程加⼊到WaitSet中,当被 Object.notify 唤醒后,会将线程从WaitSet移动到Contention List或EntryList中去。需要注意的是,当调⽤⼀个锁对象的 wait 或 notify ⽅法时,如当前锁的状态是偏向锁或轻量级锁则会先膨胀成重量级锁。