java SE6采用偏向锁以提高性能。
个人理解,偏向锁能提高性能,其理论基础是建立在绝大部分时间内,需要同步的代码其实只是一个线程在运行,正因为是单个线程在运行,所以尽量采用轻量的代码以提高性能,原子操作那是肯定需要的,但不需要采用切换上下文这类的重型操作。如果是很多线程同时运行到同步代码地方,同步代码稍长的话,采用偏向锁反而影响性能。
如果是我设计锁,首先会考虑在内存内放置锁的标志,通过原子操作(CAS--比较、设置,这两个是一个原子操作),如果没有线程获取锁,先设置锁的标志,如果别的线程已经获取锁了,先自旋(自旋是因为现在大部分是多CPU,速度很快,很多同步代码很短,执行很快,不需要进行较重的上下文切换),如果自旋一段时间后还是没有办法获取锁,则将线程加入等待队列中。这上面说起来简单,其实要实现并不容易,因为等待队列也是多线程操作,也需要同步,这本身就需要和设计的锁同样的原理。
看看openjdk是如何设计偏向锁的。
openjdk采用两个字节长的对象头作为锁,这个是在jvm分配的对象当中。这两个字节的意义如下:
这个也就是其他篇中分析过的对象头(mark word)。
上面的对象头是如何起作用的呢?
首先,通过monitorenter字节码调用,如果当前对象没有加锁,则给这个对象加上lightweight锁,会在线程的堆栈中分配一个锁记录(lock record)。锁记录包括对象原来的mark word和对象锁的所有者,在一个锁请求的过程中,一个mark word会被拷贝到锁记录中,然后对对象的mark word和锁记录中的进行原子的比较和设置操作(CAS),如何成功,则当前线程拥有锁,失败则表示其他线程已经拥有这个锁,需要进行锁的inflated操作(由轻量锁升级到重量锁)。
如果是使用偏向锁,在请求过程,会尝试CAS操作将线程ID设置到mark word中,如果成功,对象锁偏向于这个线程,如果失败,这个线程的偏向必须撤销。
如果对象锁偏向于当前线程,则当前线程对这个对象头将不再作操作。
看一下bytecodeInterpreter.cpp中对monitorenter进行解释的源码。
CASE(_monitorenter): {
oop lockee = STACK_OBJECT(-1);
BasicObjectLock* limit = istate->monitor_base();
BasicObjectLock* most_recent = (BasicObjectLock*) istate->stack_base();
BasicObjectLock* entry = NULL;
while (most_recent != limit ) {
if (most_recent->obj() == NULL) entry = most_recent;
else if (most_recent->obj() == lockee) break; //找到拥有这个对象锁的监视器
most_recent++;
}
if (entry != NULL) {
entry->set_obj(lockee);
markOop displaced = lockee->mark()->set_unlocked();
entry->lock()->set_displaced_header(displaced);
if (Atomic::cmpxchg_ptr(entry, lockee->mark_addr(), displaced) != displaced) {//原子比较,如果当前没有加锁,将displaced word 拷贝到 record word
//进入这里表示已经有线程获取锁了
if (THREAD->is_lock_owned((address) displaced->clear_lock_bits())) { //如果是当前用户拥有这个锁,则加上轻量锁
entry->lock()->set_displaced_header(NULL); //加上轻量锁
} else { //加锁不成功,继续处理
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
}
}
这个锁的偏向还是很复杂的,如果让其他线程进行等待也是一个关键的问题,有时间再做分析。附一篇偏向锁的论文供大家学习。