在JVM中,每个对象都关联这一个监视器,这里的对象包含可Object实例和Class实例.监视器是一个同步工具,相当于一个凭证,拿到这个凭证就可以进入临界区执行操作,没有拿到凭证就只能阻塞等待.重量级锁通过监视器的方式保证了任何时间内只允许一个线程通过监视器保护的临界区代码.
重量级锁的核心原理
JVM每个对象都有一个监视器,监视器随着对象一起创建 销毁.本质上监视器是一种同步工具,也可以说是一种同步机制.
1:同步.监视器所保护的临界区代码都是互斥执行的.一个监视器一个凭证,任何一个线程执行临界区代码都需要获取凭证,执行完了释放许可.
2:协作.监视器提供Signal机制,允许正在持有许可的线程释放凭证进入阻塞等待状态,等待其他线程发送Signal去唤醒.其他拥有凭证的线程可以唤醒正在阻塞等待的线程,让它可以重新获得凭证执行临界区代码.
在虚拟机中,监视器是由C++类ObjectMonitor实现的.(我对C++不是很熟,就简单介绍下,有个印象知道如何实现,如果对技术很感兴趣,可以去学学C++)
ObjectMonitor类中关键的属性是Owner(_owner) WaitSet(_WaitSet) Cxq(_cxq) EntrlList(_EntrlList).好好品味,显示锁实现原理和这个相同.
WaitSet Cxq EntrlList说明
Cxq:竞争队列,所有请求锁的线程首先被放到竞争队列里.
EntrlList:Cxq中那些有资格成为候选资源的线程被移到EntrlList中.
WaitSet:某个拥有ObjectMonitor的线程在调用Object.wait()方法之后被阻塞,然后该线程被放置在WaitSet列表中.
ObjectMonitor内部抢锁流程
1:Cxq
Cxq并不是一个真正的队列,只是一个虚拟队列,原因在于Cxq是由Node及其next指针逻辑构成,并不存在一个队列数据结构,(我自己的理解是引用,就好比1引用2,2引用3,以此类推).
每次新加入Node会在Cxq的队头进行,通过CAS改变第一个节点的指针为新增节点.同时新增节点的next指针指向后续节点.从Cxq取元素时,会从队尾获取.可以看出来,Cxq是一个无锁结构.
因为只有Owner线程才能从队尾获取元素,所以线程出队没有竞争,也避免了ABA问题.还有线程在进入Cxq之前,会通过CAS操作进行一次抢锁,获取不到才会进入队列,所以重量级锁是一个非公平锁.
2:EntrlList
EntrlList与Cxq在逻辑上都属于等待队列.Cxq会被线程并发访问为了降低对Cxq队尾的竞争,而建立了EntrlList,在Owner线程释放锁的时候,JVM会从Cxq中迁移线程到EntrlList,并会指定EntrlList中的某个线程(一般为头线程)为OnDeck Thread(Ready Thread).EntrlList里的线程作为候选者线程存在.
3:OnDeck Thread 与 Owner Thread
JVM不直接把锁传递给Owner Thread,而是把锁竞争的权利交给OnDeck Thread,OnDeck需要重新竞争锁,这样虽然牺牲了一定的公平性,但是提高了吞吐量.在JVM中也把这种行为叫做竞争切换.
OnDeck Thread线程获取到锁后会变为Owner Thread,无法获取锁的OnDeck Thread依然会留在EntrlList中,考虑到公平性,OnDeck Thread在队列中的位置不会变.
在OnDeck Thread成为Owner Thread的过程中还有一个不公平的事情,就是后来新抢锁的线程有可能直接获取锁成为WaitSet.
4:WaitSet
如果Owner Thread调用了Object.wait()方法之后就会进入WaitSet队列.直到某个时刻调用Object.notify()方法或者Object.notifyAll()唤醒,线程会重新进入EntrlList队列.
重量级锁开销
处于ContentionList EntrlList WaitSet中的线程都处于阻塞状态.线程的阻塞和唤醒都需要操作系统来帮忙.Linux内核下采用pthread_mutex_lock系统调用实现,进程要从用户态切换到内核态.
Linux系统的体系分为用户态和内核态.
Linux系统的内核是一组特殊的软件程序,负责控制计算机硬件资源.例如协调CPU资源,分配内存资源,并且提供稳定的环境供程序运行.应用程序的活动空间为用户空间,应用程序的执行必须依托于内核提供的资源,包括CPU资源 存储资源 IO资源等.
用户态与内核态有各自专用的内存空间 专用的寄存器等.进程从用户态切换至内核态需要传递许多变量 参数给内核,内核也需要保护好用户态在切换时的一些寄存器值 变量等.以便内核态调用结束后可以切换回用户态继续工作.
用户态的进程能够访问的资源受到了极大的控制,而运行在内核态的进程可以为所欲为.一个进程可以运行在用户态也可以运行在内核态,它们之间肯定有切换的方式.
用户态切换内核态的方式
1:硬件中断.硬件中断也称为外设中断,当外设完成用户请求时,会向CPU发送中断信号.
2:系统调用.其实系统调用本身就是中断,只不过是软件中断,与硬件中断不同.
3:异常.如果当前进程运行在用户态,这时发生了异常事件(例如缺页异常),就会触发切换.
用户态是应用程序运行的空间,为了能访问到内核管理的资源(例如CPU 内存 IO),可以通过内核态所提供的的访问接口实现,这些接口叫做系统调用.pthread_mutex_lock系统调用是内核态为用户态提供的Linux内核态下互斥锁的访问机制,所以使用pthread_mutex_lock系统调用时,进程需要从用户态切换到内核态,这种切换要消耗很多的时间,有可能比用户执行代码的时间还长.
重量级锁演示
public class HeavyWeightLockTest {
static final int MAX_TURN = 1000;
public static void main(String[] args) throws InterruptedException {
System.out.println(VM.current().details());
//JVM偏向锁.
Thread.sleep(5000);
ObjectLock objectLock = new ObjectLock();
//抢锁前状态.
System.out.println("抢锁前objectLock状态:");
objectLock.printObjectStruct();
Thread.sleep(5000);
CountDownLatch latch = new CountDownLatch(3);
Runnable runnable = () -> {
for (int i = 0; i < MAX_TURN; i++) {
synchronized (objectLock) {
objectLock.increase();
if (i == 0) {
System.out.println("第一个线程抢锁,lock的状态为:");
objectLock.printObjectStruct();
}
}
}
latch.countDown();
for (int j = 0; ; j++) {
LockSupport.parkNanos(10);
}
};
new Thread(runnable).start();
LockSupport.parkNanos(2000);
Runnable lightWeightRunnable = () -> {
for (int i = 0; i < MAX_TURN; i++) {
synchronized (objectLock) {
objectLock.increase();
if (i == 0) {
System.out.println(Thread.currentThread().getName()
+ "占有锁,lock的状态为:");
objectLock.printObjectStruct();
}
LockSupport.parkNanos(10);
}
}
latch.countDown();
};
//启动两个线程开始抢锁.
new Thread(lightWeightRunnable, "抢锁线程-1").start();
Thread.sleep(5000);
new Thread(lightWeightRunnable, "抢锁线程-2").start();
latch.await();
LockSupport.parkNanos(2000);
System.out.println("释放锁,lock的状态为:");
objectLock.printObjectStruct();
}
}
可以看出lock标记位为偏向锁状态,还没有偏向线程.
可以看出lock标记位101为偏向状态,并且有了偏向线程.
可以看出lock标记位000已经不是偏向锁,升级为了轻量级锁.
可以看出lock标记位变为了010成为了重量级锁.