1、前言
偏向锁是Java并发编程中一种重要的锁机制,它针对特定的线程进行优化,从而提高了并发性能。这种锁机制在多线程场景下非常常见,特别适用于一些读写分离的应用场景。
2、偏向锁
2.1、基本原理
偏向锁是一种对线程友好的锁机制,它的核心思想是通过对线程的识别和追踪,以及对锁的竞争状况进行动态分析,来决定是否启用偏向锁。
当一个线程访问一个被标记为同步块的对象时,如果该对象没有被其他线程占用,则该线程将直接获得该对象的锁;如果该对象已经被其他线程占用,则该线程将进入自旋状态,不断检查该对象是否被其他线程占用,直到获取到该对象的锁。
简单的说就是:如果一个线程获得了锁,那么锁就进入偏向模式。当这个线程再次请求锁的时候,无须再做任何同步操作。这样节省了大量关于锁申请的操作,而提升性能。这是JDK对于锁优化做的一种努力。
因此,对于几乎没有锁竞争的场合,偏向锁有较好的优化效果,因为连续多次极有可能是同一个线程请求的锁。而对于锁竞争比较激烈的场合,就不太理想了,因为在这种场合下,最有可能获得锁的都是来自不同的线程。
2.2、使用场景
偏向锁主要用于解决读写锁带来的性能问题。在读写锁中,每次读写操作都需要进行加锁和解锁操作,而这些操作会消耗大量的CPU时间。
偏向锁则通过一种乐观锁的机制,将线程分为读线程和写线程两种类型,对于读线程而言,只要没有写线程在修改共享数据,读线程就可以直接访问共享数据,而无需进行加锁和解锁操作。当有写线程在修改共享数据时,其他线程需要自旋等待,这在一定程度上提高了程序的并发性能。
3、获取偏向锁
在JDK6开始,HotSpot虚拟机就开启了-XX:UseBiasedLocking参数,默认启用了偏向锁。当锁对象第一次被线获取的时候,虚拟机将会把对象头中的标志位设置为“01”、把偏向模式设置为“1”,表示进入偏向模式。同时使用CAS操作把获取到这个锁的线程的ID记录在对象的 Mark Word之中。如果 CAS 操作成功,持有偏向锁的线程以后每次进入这个锁相关的同步块时,虚拟机都可以不再进行任何同步操作(例如加锁、解锁及对 Mark Word 的更新操作等)。
一旦出现另外一个线程去尝试获取这个锁的情况,偏向模式就马上宣告结束。根据锁对象目前是否处于被锁定的状态决定是否撤销偏向(偏向模式设置为“0”),撤销后标志位恢复到未锁定(标志位为“01”)或轻量级锁定(标志位为“00”)的状态。
梳理一下流程图:
我们可以看到,当对象进入偏向状态的时候,Mark Word的大部分空间都用于存储持有锁的线程ID了,原有的这部分空间是存储的对象哈希码。
实际上,偏向锁的对象头中是没有空间存储hash值的。
jvm中对偏向锁的实现如下:
代码语言:javascript复制// 偏向锁加锁操作
bool BiasedLocking::biased_locking_acquire(oop obj, JavaThread* thread) {
int thread_id = get_thread_id();
int* lock_word = get_header_address(obj);
// 检查是否已经存在锁拥有者
if (*lock_word & 0x1) {
return false; // 已经存在锁拥有者,直接返回false
}
// 检查是否需要升级为重量锁
if ((*(lock_word) & 0x2) == 0) {
if (Atomic::cmpxchg(EXPECTED_BIAS, lock_word, lock_word - 1) != (int)thread_id - 1) {
return false; // 升级失败,锁已经被其他线程获取,直接返回false
} else {
// 升级成功,将线程ID写入锁拥有者字段中,并标记为重量锁
*lock_word = thread_id | 0x2;
thread->set_next(NULL); // 断开与队列中其他线程的连接
return true; // 升级成功,返回true
}
} else {
return true; // 已经是重量锁,直接返回true
}
}
// 偏向锁解锁操作
void BiasedLocking::biased_locking_release(oop obj, JavaThread* thread) {
int thread_id = get_thread_id();
int* lock_word = get_header_address(obj);
if (*lock_word != 0) {
if ((*lock_word) & 0x2) {
// 如果是重量锁,直接将线程ID写入锁拥有者字段中,并标记为无锁状态
Atomic::cmpxchg((int)thread_id, lock_word, lock_word - 1);
} else {
// 如果是偏向锁,则将线程ID写入锁拥有者字段中,并标记为无锁状态,同时唤醒等待队列中的线程
Atomic::cmpxchg((int)thread_id, lock_word, lock_word - 1);
BiasedLocking::Biased_locking_unpark(thread);
}
} else {
// 如果已经是无锁状态,则直接返回,不做任何操作
}
}
4、何时撤销
4.1、到达安全点
偏向锁的撤销需要等待全局安全点(safe point),此时会暂停所有线程,然后检查持有偏向锁的线程是否还活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,则需要遍历持有偏向锁的栈,检查是否存在其他对象头和该对象头不一致,如果存在,则需要重新偏向该线程。最后唤醒暂停的线程。
4.2、其他线程尝试竞争偏向锁
前面也提到了。当出现另一个线程尝试获取偏向锁的情况下,持有偏向锁的线程才会释放锁,线程不会主动释放偏向锁。
4.3、重新计算hashcode
前面对象头里面我们可以看到,当一个对象持有偏向锁时,对象头是不存储hashcode的。那么原先存储的对象hashcode怎么办呢?实际上,当一个对象计算过一致性hash后,就再也无法进入偏向锁状态了。而当一个对象当前正处于偏向锁状态,又收到需要计算其一致性哈希码请求时,它的偏向状态会被立即撤销,并且锁会膨胀为重量级锁。在重量级锁的实现中对象头指向了重量级锁的位置,代表重量级锁的 ObjectMonitor类里有字段可以记录非加锁状态(标志位为“01”)下的Mark Word,其中自然可以存储原来的哈希码。
5、小结
偏向锁可以提高带有同步但无竞争的程序性能,但它同样是一个带有效益权衡(TradeOff)性质的优化,也就是说它并非总是对程序运行有利。如果程序中大多数的锁都总是被多个不同的线程访问,那偏向模式就是多余的。在具体问题具体分析的前提下,有时候使用参数-XX:-UseBiasedLocking 来禁止偏向锁优化反而可以提升性能。