reentrantLock公平锁和非公平锁源码解析

ReentrantLock非公平锁源码分析

ReentrantLock lock = new ReentrantLock();
lock.lock();

上面这段代码是最简单的获取ReentrantLock的非公平锁的代码,我们来看看这段代码后面的源码是如何运行的。

final void lock() {
	//首先尝试看看能不能获取到锁,如果CAS成功,那么就获取到了
   if (compareAndSetState(0, 1))
   		//如果获取到了,就记录一下当前的线程,以便后面的重入
        setExclusiveOwnerThread(Thread.currentThread());
    else
    	//如果CAS失败,则调用acurire方法
        acquire(1);
}
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
        selfInterrupt();
}

这边的话首先会再尝试获取锁,如果获取到了,返回true,就会退出if语句,如果没有获取到,那么则将当前线程添加到队列中,并循环获取锁,直到获取到为止。

下面是tryAcquire方法的源码:

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    //获取到锁的标志状态
    int c = getState();
    if (c == 0) {
    	//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;
}

如果尝试获取锁失败了,则先会将当前线程添加到一个队列中:

private Node addWaiter(Node mode) {
	//使用当前线程创建一个Node节点,mode分为共享和排他
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    if (pred != null) {
    	//如果队列中已经存在节点了,那么直接将该节点添加到后面
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //如果是第一次构建队列,则调用该方法
    enq(node);
    return node;
}
private Node enq(final Node node) {
	for (;;) {
	    Node t = tail;
	    if (t == null) { // Must initialize
	    	//如果尾节点还是空的,那么构建一个空节点做为头节点
	    	//然后在下一次循环的时候进入到else
	        if (compareAndSetHead(new Node()))
	            tail = head;
	    } else {
	    	//和上面一样,将当前线程构建的节点添加到队列的尾部
	        node.prev = t;
	        if (compareAndSetTail(t, node)) {
	            t.next = node;
	            return t;
	        }
	    }
	}
}

到这里,队列已经构建完成了,那么这个队列中的节点在有锁释放后,又是怎么去争夺锁资源的呢?

final boolean acquireQueued(final Node node, int arg) {、
	//标识获取锁的过程中是否出现了异常
	boolean failed = true;
	 try {
	 	//标识线程在等待唤醒的时候是否被打断(interrupt)
	     boolean interrupted = false;
	     //这里会循环获取锁,知道获取到或者出现异常
	     for (;;) {
	         final Node p = node.predecessor();
	         //如果当前线程为第一个节点(排除head节点),则尝试获取锁
	         if (p == head && tryAcquire(arg)) {
	         	//如果获取到了,则将当前节点设置为head
	             setHead(node);
	             p.next = null; // help GC
	             failed = false;
	             return interrupted;
	         }
	         //判断节点是否需要park,如果需要,则进入park
	         if (shouldParkAfterFailedAcquire(p, node) &&
	             parkAndCheckInterrupt())
	             interrupted = true;
	     }
	 } finally {
	     if (failed)
	         cancelAcquire(node);
	 }
}

这段代码有一点比较难理解,就是首先会判断当前线程的前一个节点是否为head节点,如果是的话,才会尝试获取锁。但是非公平锁不是应该所有的节点都去争夺锁资源吗?其实非公平是作用于新入的节点,而已经调用过addWaiter方法的节点,则需要排队

PS:park方法与wait方法的作用类似。

下面我们再来看一下ReentrantLock是如何来判断当前线程获取锁失败后是否需要进入到park状态。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
    	//如果前节点是SIGNAL状体,则代表需要park
        return true;
    if (ws > 0) { 
    	//如果waitStatus的值大于0,代表已取消,需要将无效的节点删除
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
    	//否则将前节点设置为SIGNAL状态
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

上面的代码很好理解,但是需要知道Node的waitStatus的几种状态的含义。

  • CANCELLED:值为1,在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该Node的结点,其结点的waitStatus为CANCELLED,即结束状态,进入该状态后的结点将不会再变化。
  • SIGNAL:值为-1,被标识为该等待唤醒状态的后继结点,当其前继结点的线程释放了同步锁或被取消,将会通知该后继结点的线程执行。说白了,就是处于唤醒状态,只要前继结点释放锁,就会通知标识为SIGNAL状态的后继结点的线程执行。
  • CONDITION:值为-2,与Condition相关,该标识的结点处于等待队列中,结点的线程等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
  • PROPAGATE:值为-3,与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。
  • 0状态:值为0,代表初始化状态。

下面再来看看parkAndCheckInterrupt方法:

private final boolean parkAndCheckInterrupt() {
	//通过LockSupport类来park该线程
    LockSupport.park(this);
    //将park的线程唤醒可能是调用unpark方法,也可能是被打断了
    return Thread.interrupted();
}

到这里,非公平锁的整个获取流程就结束了。

扫描二维码关注公众号,回复: 9348040 查看本文章

ReentrantLock非公平锁实现原理小结

通过上面的源码解析,我们已经能够大致了解ReentrantLock是如何实现非公平锁的了,下面我们来个小结。

ReentrantLock流程图
这里有一点需要注意,就是在最后的时候,如果线程获取到了锁,并且中途被打断过,这个时候线程会调用interrupte(),但是这个是没有任何意义的,因为运行中的线程是不能被打断的。
当然,ReentrantLock也是支持被打断的,具体可参考:

ReentrantLock公平锁源码解析

基于上面的内容,我们已经理解了ReentrantLock非公平锁的实现原理,其实公平锁和非公平锁的源码差不多,只有两处不一样。
第一处:

final void lock() {
	acquire(1);
}

这里就是调用lock方法的入口,和非公平锁不一样的是非公平锁会先去尝试通过CAS获取锁,但是公平锁不会。

第二处:

protected final boolean tryAcquire(int acquires) {
   final Thread current = Thread.currentThread();
   int c = getState();
   if (c == 0) {
       if (!hasQueuedPredecessors() &&
           compareAndSetState(0, acquires)) {
           setExclusiveOwnerThread(current);
           return true;
       }
   }
   else if (current == getExclusiveOwnerThread()) {
       int nextc = c + acquires;
       if (nextc < 0)
           throw new Error("Maximum lock count exceeded");
       setState(nextc);
       return true;
   }
   return false;
}

这段代码是尝试去获取锁,可以看出,这边当state为0的时候,也就是无锁状态的时候,会先去判断当前线程的是否构建了节点且节点是否为head后的第一个节点,如果是,才会去尝试CAS获取锁。

public final boolean hasQueuedPredecessors() {
    Node t = tail; // Read fields in reverse initialization order
    Node h = head;
    Node s;
    //只有当没有任何节点获取锁或者本节点为head后第一个节点
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

从这里我们也能看出公平锁和非公平锁的区别:公平锁的新入线程不能直接获取锁,必须去排队(除非没有任何竞争发生)。而非公平锁新入的线程则可以先尝试获取锁,如果失败了再排队

发布了81 篇原创文章 · 获赞 16 · 访问量 20万+

猜你喜欢

转载自blog.csdn.net/mazhen1991/article/details/90450790