《Java后端知识体系》系列之独占锁 Reentrant Lock 的原理

周末两天跟女朋友出去high了一下,所以自己的知识整理也搁置了两天,手动狗头!

ReentrantLock概览

ReentrantLock是可重入的独占锁,所以只能有一个线程获得该锁,,其它获取该锁的线程会被阻塞而被放入该锁的AQS阻塞队列中,以下是ReentrantLock的类图。
在这里插入图片描述
从以上类图中可以看到,ReentrantLock最终还是使用AQS来实现的,并且根据参数来决定其内部是一个公平锁还是非公平锁,默认是非公平锁。

public ReentrantLock(){
	sync = new NofairSync();
}
public ReentrantLock(boolean fair){
	sync = fair ? new FairSync() : new NofairSync();
}

其中Sync类直接继承自AQS,它的子类NofairSync和FairSync分别实现了获取锁的非公平与公平策略。
在这里AQS的state状态值表示线程获取锁的可重入次数,在默认情况下state的值为0表示当前锁没有任何线程持有。当一个线程第一次获取该锁时会尝试使用CAS来设置state的值为1,如果CAS成功则当前线程获取了该锁,然后记录该锁的持有者为当前线程。在线程没有释放锁的情况下第二次获取该锁后,state被设置为2,这就是可重入次数。在该线程释放的时候,会通过CAS操作让状态值state减1,如果减1后状态值为0,则释放该锁,否则继续释放锁直到state为0才最终放弃锁。

获取锁

1、void lock()方法

当一个线程调用该方法时,说明该线程希望获取该锁。

  • 如果锁当前没有被其它线程占用并且当前线程没有获取过该锁,则当前线程会获取到该锁,然后设置当前锁的持有者为当前线程,并且设置state的值为1,然后直接返回。
  • 如果当前线程已经持有了该锁,则只是通过CAS操作将state加1,然后返回。
  • 如果该锁已经被其它线程持有,那么调用该方法的线程会被放入AQS队列后阻塞挂起。
public void lock(){
	sync.lock();
}

在如上代码中。ReentrantLock的lock()委托给了sync类,根据创建ReentrantLock构造函数选择sync的实现是NofairSync还是FairSync,这个锁是一个非公平锁还是公平锁。这里先看sync的子类NofairSync的情况,也就是非公平锁时。

final void lock(){
	//(1)CAS设置状态值state
	if(compareAndSetState(0,1))
		//设置锁的持有者为当前线程
		setExclusiveOwnerThread(Thread.currentThread());
	else
	//(2)调用AQS的acquire方法
		acquire(1);

在以上代码中,因为默认state的值为0,所以第一个调用Lock的线程会通过CAS设置状态值为1,CAS成功则表示当前线程获取到了锁,然后通过setExclusiveOwnerThread设置该锁的持有者为当前线程。
如果这时候有其它线程调用lock方法企图获取该锁,CAS会失败,然后调用AQS的acquire方法。注意,传递参数为1

public final acquire(int arg){
	//(3)调用ReentrantLock重写的tryAcquire方法
	//tryAcquire返回false会把当前线程放入AQS的阻塞队列中,通过addWaiter方法将当前线程放入AQS的阻塞队列中
	if(!tryAcquire(arg)&&acquireQueued(addWaiter(Node.EXCLUSIVE),arg))
		selfInterrupt();

之前说过AQS并没有提供可用的tryAcquire方法,tryAcquire方法需要子类自己定制化,所以这里代码(3)会调用ReentrantLock重写的tryAcquire方法。接下来看非公平锁的代码:

protect final boolean tryAcquire(int acquires){
	return nonfairTryAcquire(acquires);
}
final boolean nonfairTryAcquire(int acquires) {
  final Thread current = Thread.currentThread();
   int c = getState();
   //(4)当前AQS的状态值
   if (c == 0) {
       if (compareAndSetState(0, acquires)) {
           setExclusiveOwnerThread(current);
           return true;
       }
   }
   //(5)当前线程是该锁的持有者
   else if (current == getExclusiveOwnerThread()) {
       int nextc = c + acquires;
       if (nextc < 0) // overflow
           throw new Error("Maximum lock count exceeded");
       setState(nextc);
       return true;
   }
   return false;
}
  • 首先代码4中会查看当前锁的状态值是否为0,为0则表示该锁空闲,那么就通过CAS尝试获取锁,将state的值设置为1,并设置锁的占有者为当前线程,然后返回true。如果当前状态值不为0,则说明该锁已经被其它线程占用,所以代码5会判断当前锁的占有者是否为当前线程,如果锁的持有者为当前线程则state加1,然后返回true,这里需要注意nextc<0说明可重入次数溢出了。如果当前线程不是锁的持有者则返回false,然后将线程放入AQS阻塞队列。
  • 介绍完非公平锁的实现代码,现在看看非公平锁是怎么体现的,首先非公平是说先尝试获取锁的线程并不一定比后尝试获取锁的线程优先获取锁。
  • 假设线程A先调用lock时执行nonfairTryAcquire的代码4发现当前状态值不为0,所以执行代码5发现锁的持有者不是自己,则返回false,然后被放入阻塞队列。
  • 然后线程B调用lock方法,执行nonfairTryAcquire,发现当前state的值为0了(其它线程此时刚好已经释放了该锁),所以通过CAS获取到了该锁。
  • 虽然线程A先尝试获取锁,但是后来的线程B却获得了锁,这就是非公平的体现。这里线程B在获取锁前并没有查看当前AQS阻塞队列中是否有比自己更早请求该锁的线程,而是使用了抢夺策略。那么接下来看看公平锁是如何实现的。
        /**
         * Fair version of tryAcquire.  Don't grant access unless
         * recursive call or no waiters or is first.
         */
        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            //(7)判断当前状态值是否为0
            if (c == 0) {
            	//(8)公平策略
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //(9)当前线程是该锁的持有者
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }
  • 如上代码中,公平的tryAcquire策略与非公平的类似,不同之处在于,代码8在设置CAS前添加了hasQueuedPredecessors方法,该方法是实现公平锁的核心,代码如下
public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }
  • 在如上代码中如果当前线程节点有前驱节点则返回true,否则设置当前AQS队列为空或者当前线程是AQS的第一个节点则返回false。如果h==t则说明当前队列为空,直接返回false,如果h!=t并且s=null则说明有一个元素将要作为AQS的第一个节点入队,那么返回true,如果h!=t并且s!=null和s.thread!=Thread.currentThread()则说明队列里面的第一个线程不是该线程则返回true。

2、void locklnterruptibly()方法

该方法与lock方法类似,它的不同在于它是对中断进行响应,就是当前线程在调用该方法时,如果其它线程调用了当前线程的interrupt方法,则当前线程会抛出InterruptedException异常,然后返回

public void lockinterruptibly() throws InterruptedException{
	sync.acquirelnterruptibly(1);
}
public final void acquireinterruptibly(int arg)throws InterruptedException{
	//如果当前线程被中断,则直接抛出异常
	if(Thread.interrupted())
		throws new terruptedException();
	//尝试获取资源
	if(!tryAcquire(arg)){
		//调用AQS可被中断的方法
		doAcquireinterruptibly(arg);
	}

3、boolean trylock()方法

尝试获取锁,如果当前该锁没有被其它线程占用,则当前线程获取该锁并返回true,否则返回false。注意,该方法不会引起当前线程阻塞。

 public boolean tryLock() {
        return sync.nonfairTryAcquire(1);
    }
    final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }

如上代码中与非公平锁的tryAcquire()方法类似,所以tryLock()使用的是非公平策略。

4、boolean tryLock(long timeout,TimeUnit unit)方法

尝试获取锁,与tryLock不同之处在于,它设置了超时时间。如果超时时间到了却还没获取到锁则返回false。

  public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
    }

释放锁

1、void unlock()方法

尝试释放锁,如果当前线程持有该锁,则调用该方法会让该线程对线程持有的AQS状态值减1,如果减1之后状态值为0,则当前线程会释放该锁,否则仅仅减1(重入锁),需要继续释放锁,直到状态值为0。如果当前线程没有持有该锁,而调用了该方法则会抛出IllegalMonitorStateException异常,代码如下:

    public void unlock() {
        sync.release(1);
    }
    protected final boolean tryRelease(int releases) {
    	//(11)如果不是锁的持有者调用unlock则抛出异常
       int c = getState() - releases;
       if (Thread.currentThread() != getExclusiveOwnerThread())
           throw new IllegalMonitorStateException();
       boolean free = false;
       //(12)如果当前可重入次数为0,则清空持有线程
       if (c == 0) {
           free = true;
           setExclusiveOwnerThread(null);
       }
       //(13)设置可重入次数原始值减1
       setState(c);
       return free;
   }

如代码11中如果当前线程不是该锁的持有者,则直接抛出异常,否则查看状态值是否为0,为0则说明当前线程要放弃该锁的持有全,则执行代码12把当前锁的持有者设为null。如果状态值不为0,则仅仅让当前线程对锁的可重入次数减1。

以上就是整理的ReentrantLock的知识点,自己也梳理了整个流程。

自己依然是个会敲代码的汤姆猫!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章