这是我参与8月更文挑战的第11天,活动详情查看:8月更文挑战
轻量级锁适用于临界区的代码执行时间很短的情况,如果临界区的代码执行的时间很长,那么势必会造成其他争抢锁的线程会进行很长时间的CPU空转,这就与轻量级锁的设计背道而驰了,因此,如果临界区代码执行的时间如果很长的话,使用互斥锁将争抢锁的线程先挂起的性能要好于轻量级锁。
重量级锁的原理
在java虚拟机中,JVM会为每个对象都关联一个监视器Monitor
对象,这个监视器对象随着对象的创建而创建,随着对象的销毁而销毁,这个监视器对象就是用来保证在同一时刻,只有一个线程可以访问临界资源。
Monitor的主要作用
1. 同步
Monitor相当于是一个"许可证",任何线程想访问临界资源,都必须先获取到这个许可证,并在访问完临界资源后,归还这个"许可证"。
2. 协作
Monitor同时也提供了协作的机制,正在执行同步代码的线程可以释放掉自己获取到的"许可证"并进行休眠,其他线程可以再次获取这个"许可证"并唤醒处于休眠的线程,进而达到线程协作的目的。
Monitor对象在HotSpot中的结构
通过openjdk中的ObjectMonitor.hpp
文件可以看到Monitor
在虚拟机中的结构如下:
ObjectMonitor::ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0;
// 线程的重入次数
_recursions = 0;
_object = NULL;
// 标识拥有该monitor的线程
_owner = NULL;
// 等待线程组成的双向循环链表
_WaitSet = NULL;
_WaitSetLock = 0;
_Responsible = NULL;
_succ = NULL;
// 多线程竞争锁进入时的单向链表
cxq = NULL;
FreeNext = NULL;
// _owner从该双向循环链表中唤醒线程节点
_EntryList = NULL;
_SpinFreq = 0;
_SPinClock = 0;
OwnerIsThread = 0;
}
核心属性介绍
- owner 持有锁的线程
- recursions 线程的重入次数
- waitSet 调用wait方法后线程被放入的队列
- cxq 请求锁的线程首先被放入的队列
- EntryList cxq队列中有资格能获取到锁的线程被移动到的队列
线程抢占锁流程
-
Cxq 竞争队列
每次新加入 Node 会在 Cxq 的队头进行,通过 CAS 改 变第一个节点的指针为新增节点,同时设置新增节点的 next 指向后续节点;从 Cxq 取得元素时,会从队尾获取。因为只有 Owner 线程才能从队尾取元素,也即线程出列操作无争用,当然也就避免了 CAS 的 ABA 问题。线程进入 Cxq 前,抢锁线程会先尝试通过 CAS 自旋获取锁,如果获取不到,才就进入 Cxq 队列,这明显对于已经进入 Cxq 队列的线程是不公平的。所以,synchronized 同步块所使用的重量级锁是不公平锁。
-
EntryList
EntryList 与 Cxq 在逻辑上都属于等待队列。Cxq 会被线程并发访问,为了降低对 Cxq 队尾的 争用,而建立 EntryList。在 Owner 线程释放锁时,JVM 会从 Cxq 中迁移线程到 EntryList,并会 指定 EntryList 中的某个线程(一般为 Head)为 OnDeck Thread(Ready Thread)。EntryList 中的线程,作为候选竞争线程而存在。
-
OnDeck Thread 与 Owner Thread
JVM 不直接把锁传递给 Owner Thread,而是把锁竞争的权利交给 OnDeck Thread,OnDeck 需要重新竞争锁。这样虽然牺牲了一些公平性,但是能极大的提升系统的吞吐量,在 JVM 中,也把这种选择行为称之为“竞争切换”。在 OnDeck Thread 成为 Owner 的过程中,还有一个不公平的事情,就是后来的新抢锁线程可能直接通过 CAS 自旋成为 Owner 而抢到锁
-
WaitSet
Owner 线程被 Object.wait()方法阻塞,则转移到 WaitSet 队列中,直到某个时刻通过 Object.notify()或者 Object.notifyAll()唤醒,在线程会重新进去 EntryList 中。
重量级锁演示
@Test
public void test2() throws InterruptedException {
TimeUnit.SECONDS.sleep(5);
Foo foo = new Foo();
System.err.println(ClassLayout.parseInstance(foo).toPrintable());
new Thread(new LockTest(foo)).start();
new Thread(new LockTest(foo)).start();
TimeUnit.SECONDS.sleep(2);
System.err.println(ClassLayout.parseInstance(foo).toPrintable());
}
打印结果:
org.ywb.Foo object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000005 (biasable; age: 0)
8 4 (object header: class) 0xf8011d21
12 4 int Foo.i 0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
Thread-1-org.ywb.Foo object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00007fca4b80d3da (fat lock: 0x00007fca4b80d3da)
8 4 (object header: class) 0xf8011d21
12 4 int Foo.i 1
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
Thread-2-org.ywb.Foo object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x00007fca4b80d3da (fat lock: 0x00007fca4b80d3da)
8 4 (object header: class) 0xf8011d21
12 4 int Foo.i 2
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
org.ywb.Foo object internals:
OFF SZ TYPE DESCRIPTION VALUE
0 8 (object header: mark) 0x0000000000000001 (non-biasable; age: 0)
8 4 (object header: class) 0xf8011d21
12 4 int Foo.i 2
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total
解释:
- 第一次打印,此时对象处于偏向锁状态,但是并没有偏向某个线程,所以偏向线程id和偏向时间戳都是空的
因为两个线程同时启动,同时去争取锁,导致锁膨胀成为了重量级锁
- 第二次打印,线程名称为
Thread-1
的线程获取到了重量级锁,对变量进行了加1操作 - 第三次打印,线程名称为
Thread-2
的线程获取到了线程Thread-1
的锁,对变量进行了加1操作 - 第四次打印,因为这时两个线程都释放了重量级锁,所以此时对象处于无锁状态。
重量级锁的系统开销
处于 ContentionList
、EntryList
、WaitSet
中的线程都处于阻塞状态,线程的阻塞或者唤醒都 需要操作系统来帮忙,Linux 内核下采用 pthread_mutex_lock
系统调用实现的,进程需要从用户态 切换到内核态pthread_mutex_lock
系统调用,是内核态为 用户态进程提供的 Linux 内核态下互斥锁(Mutex)的访问机制,所以使用 pthread_mutex_lock
系统调用时,进程需要从用户态切换到内核态,而这种切换是需要消耗很多时间的,有可能比用户执行代码的时间还要长。