ReentrantLock执行详细流程

 
 
ReentrantLock介绍

ReentrantLock一个可重入的互斥锁,又称为“独占锁”;ReentrantLock锁在同一个时间点只能被一个线程锁持有;而可重入的意思是,ReentrantLock锁,可以被单个线程多次获取;ReentrantLock在同一个时间点只能被一个线程获取(当某线程获取到“锁”时,其它线程就必须等待);ReentraantLock是通过一个FIFO的等待队列来管理获取该锁所有线程的。

关键词介绍

CLH队列 -- Craig, Landin, and Hagersten lock queue

    CLH队列是AQS中“等待锁”的线程队列。在多线程中,为了保护竞争资源不被多个线程同时操作而起来错误,我们常常需要通过锁来保护这些资源。在独占锁中,竞争资源在一个时间点只能被一个线程锁访问;而其它线程则需要等待。CLH就是管理这些“等待锁”的线程的队列。    CLH是一个非阻塞的 FIFO 队列。也就是说往里面插入或移除一个节点的时候,在并发条件下不会阻塞,而是通过自旋锁和 CAS 保证节点插入和移除的原子性。

CAS函数 -- Compare And Swap 

    CAS函数,是比较并交换函数,它是原子操作函数;即,通过CAS操作的数据都是以原子方式进行的。例如,compareAndSetHead(), compareAndSetTail(), compareAndSetNext()等函数。它们共同的特点是,这些函数所执行的动作是以原子的方式进行的。

下文先根据3种不同情形看ReentrantLock获取锁的详细流程,最后再说释放锁的流程

            1、单独一个线程获取锁lock() ; 2、同一个线程线程执行两次lock()  ;3、两个不同线程各执行一次lock();4、释放锁

一、一个线程获取锁lock()的流程

public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();
        reentrantLock.lock();
        

//如果就一个线程获取锁,比较简单。可以看到首先通过CAS机制询问内存值是否为0,是即更改内存值为1,然后设置锁的拥有者为当前线程。这样既拿到了锁。

二、同一个线程线程执行两次lock(),验证线程“可重入”机制的执行流程

public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();
        reentrantLock.lock();//主线程
        reentrantLock.lock();//主线程,介绍的是这一行具体流程
//下段介绍的是主线程第二次执行lock()的具体流程,先通过CAS比较内存值是否为0,此时不满足条件,然后通过acquire(1)尝试获取锁。因为可重入机制,即在 tryAcquire()方法尝试获取锁,是可以尝试成功的返回true。


说明:“主线程”是通过acquire(1)获取锁的。
        这里说明一下“1”的含义,它是设置“锁的状态”的参数。每获取一次锁(同一个线程多次获取锁也会递增+1)则会递增加+1。

tryAcquire()的作用就是尝试去获取锁。尝试成功的话,返回true;尝试失败的话,返回false,因为可重入机制,即尝试获取成功,返回true

三、两个不同线程各执行一次lock(),主线程执行了一次lock()之后,接着子线程也执行一次lock()。验证线程的“互斥性”

public static void main(String[] args) {
        ReentrantLock reentrantLock = new ReentrantLock();
        reentrantLock.lock();//主线程
        new Thread(() -> reentrantLock.lock()).start();//子线程

下面介绍的是子线程执行lock()的具体流程,过程中“主线程”已经拿到了锁,“子线程”会加入到等待队列,待主线程释放了锁之后,子线程才会继续执行。


---------------------------> acquire -> tryAcquire

因为锁已经被主线程拥有,子线程再去尝试锁,则会失败,返回false



---------------------------> acquire -> addWaiter(Node.EXCLUSIVE)

    addWaiter(Node.EXCLUSIVE)的作用是,创建“子线程”的Node节点,并且将该节点添加到CLH队列的末尾。


---------------------------> acquire -> addWaiter(Node.EXCLUSIVE) -> enq(node)

enq()的作用是,如果CLH队列为空,则新建一个CLH表头;然后将node添加到CLH末尾。否则,直接将node添加到CLH末尾。此时队列是为空的会走到enq方法,创建一个CLH空表头,将子线程对应的节点加入到CLH末尾。



---------------------------> acquire -> acquireQueued

前面addWaiter()方法已经将“子线程”线程添加到CLH队列中了。而acquireQueued()的作用就是逐步的去执行CLH队列的线程,如果“子线程”线程获取到了锁,则返回;否则“子线程”线程进行休眠,直到唤醒并重新获取锁了才返回。



说明
(01) 关于waitStatus请参考下表(中扩号内为waitStatus的值)。

CANCELLED[1]  -- 当前线程已被取消
SIGNAL[-1]    -- “当前线程的后继线程需要被unpark(唤醒)”。一般发生情况是:当前线程的后继线程处于阻塞状态,而当前线程被release或cancel掉,因此需要唤醒当前线程的后继线程。
CONDITION[-2] -- 当前线程(处在Condition休眠状态)在等待Condition唤醒
PROPAGATE[-3] -- (共享锁)其它线程获取到“共享锁”
[0]           -- 当前线程不属于上面的任何一种状态。



如果子线程被中断过,parkAndCheckInterrupt方法中的Thread.interrupted()返回了true,则“子线程”自己产生一个中断。

为什么要自己产生一个中断呢?

    因为Thread.interrupted()会清除中断状态,所以要重新产生一个中断


总结

    “子线程”首先通过tryAcquire()尝试获取锁,尝试失败后,执行addWaiter(Node.EXCLUSIVE)来将“子线程”加入到"CLH队列"末尾。然后调用acquireQueued()会进入到CLH队列中休眠等待,直到获取锁了才返回!如果“子线程”在休眠等待过程中被中断过,acquireQueued会返回true,此时"子线程"会调用selfInterrupt()来自己给自己产生一个中断。

四、释放锁

public static void main(String[] args) {
	ReentrantLock reentrantLock = new ReentrantLock();
	reentrantLock.lock();//主线程
	new Thread(() -> reentrantLock.lock()).start();//子线程
	reentrantLock.unlock();//测试此段代码

release()会先调用tryRelease()来尝试释放当前线程锁持有的锁。成功的话,则唤醒后继等待线程,并返回true。否则,直接返回false。

tryRelease()尝试释放锁


unparkSuccessor()的作用是“唤醒当前线程的后继线程”。后继线程被唤醒之后,就可以获取该锁并恢复运行了。


总结

“释放锁”的过程比较简单。释放锁时,主要进行的操作,是更新当前线程对应的锁的状态。如果当前线程对锁已经彻底释放,则设置“锁”的持有线程为null,设置当前线程的状态为空,然后唤醒后继线程。

参考文献:http://www.cnblogs.com/skywang12345/p/3496147.html#p1

猜你喜欢

转载自blog.csdn.net/qq1010267837/article/details/79654438