ReentrantLock底层源码分析

synchronized关键字的扩展:重入锁(ReentrantLock):

public class ReentrantLockDemo implements Runnable{
    public static ReentrantLock lock = new ReentrantLock();
    public static int i = 0;
    public void run(){
        for(int j=0;j<10000;i++){
            lock.lock();
            try{
                i++;
            }finally{
                lock.unlock();
            }
        }
    }
}

与关键字synchronized相比,重入锁有着明显的灵活性,何时加锁,何时放锁,重入锁之所以叫重入锁,是因为这种锁可以反复进入。当然,这种反复仅仅局限于一个线程。比如可以这样写:

lock.lock();
lock.lock();
try{
    i++;
}finally{
    lock.unlock();
    lock.unlock();
}

一个线程连续两次获得同一把锁是允许的。

重入锁的中断响应:

对于关键字synchronized来说,如果一个线程在等待锁,那么结果只有两种情况,要么它获得这把锁继续执行,要么它就保持等待。而使用重入锁,这个线程还可以被中断。也就是在等待锁的过程中,程序可以根据需要取消对锁的请求。这个对处理死锁是有一定帮助的。

下面我们来从源码正式分析ReentrantLock的原理:
我们一路点进lock()方法:我们会发现abstract void lock();我们看看实现它的类
在这里插入图片描述
这个就是熟悉的公平锁与非公平锁,我们先来看看公平锁
一路点到:acquire这个方法:

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

点进tryAcquire,这个方法的意思是:尝试加锁

  @ReservedStackAccess
  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;
          }
      }
      /**
      *	这里体现重入,当current这个值等于getExclusiveOwnerThread()方法的返回值(这个方法是获取并返回当前持有锁的线程)
      *	就给状态值加1
      */
      else if (current == getExclusiveOwnerThread()) {
          int nextc = c + acquires;
          if (nextc < 0)
              throw new Error("Maximum lock count exceeded");
          setState(nextc);
          return true;
      }
      return false;
}

哪里公平呢?

ReentranLock在加锁时执行到上面的tryAcquire方法,比如现在有一个线程被要lock(),会先定义一个变量获取一个状态值,这个状态就是现在这个锁是自由的还是被持有的,显然第一个线程进来锁一定是自由的等于0,判断c==0一定是成立的,于是向下执行,请看这个函数 !hasQueuedPredecessors()。当第一个线程进来的时候,会先判断这个函数,这个函数的意义是判断当前队列中,有没有早早已经来等锁的线程,换句话说有没有必要将当前正在执行的线程加入队列,如果没有,那么返回false,再取非,于是!hasQueuedPredecessors()返回true,所有才有机会执行CAS改变状态获取锁,这时候假设我们有第二个线程来竞争,线程一还没有释放锁,所以状态码是1,第二个线程判断c!=0,会直接返回false,然后把它加入同步队列进行等待。

所以公平就公平在!hasQueuedPredecessors()这个方法会进行判断从而去调度线程,这里面涉及到一个著名的数据结构,AQS队列,这个队列就是把阻塞的线程都存在里面,唤醒的时候,先加进来的线程先唤醒,这就是公平,我们来看看非公平锁。

下面是非公平锁:非公平锁是的加锁函数是nonfairTryAcquire

final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
            	//这里少了!hasQueuedPredecessors()
                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;
        }

看到没有!!!!!与公平锁相比,就少了!hasQueuedPredecessors()这个函数
我们看到了比公平锁少了!hasQueuedPredecessors()这个函数,那么就意味着,一个线程拿锁时,不会知道有没有比他最先来的但是已经在等待的线程,也就是说当几个线程竞争时,假定有两个线程已经被加入到了同步队列,这时候一个线程进来抢走了锁,那么前两个在队列中的线程,没错!白!等!了!,但是那个线程一来就把锁抢走了,这就体现了不公平。

在说这个很重要的方法!hasQueuedPredecessors()之前我们需要先搞明白,一个被阻塞的线程是怎么成为一个Node入队的,我们以公平锁为例。这里我们需要一个Node类,这里只列举一些重要的属性:

class Node{
    volatile Node prev;
    volatile Node next;
	volatile Thread thread;   
	private transient volatile Node head;
    private transient volatile Node tail;
    volatile int waitStatus;
}

假设有两个线程t1,t2来了,且存在竞争,当t1先拿到锁的执行权,很轻松加锁成功并且把锁的状态改为1,没释放锁的时候,t2来了,判断锁的状态为1,这时候条件不成立,所以它会返回false,也就是说tryAcquire方法会返回false。这时候我们再放上acquire这个方法

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

我们可以看到 !tryAcquire(arg)就会返回true因为tryAcquire返回了false,所以第一个条件成立,于是开始先调用addWaiter方法。

private Node addWaiter(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;
    }

这个方法就是入队操作,tail在这时候等于null,所以会直接执行enq方法,注意这时候node就是当前正在执行的t2线程。

private Node enq(final Node node) {
        for (;;) {
            Node t = tail;
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

在enq方法中,这是一个死循环,tail一定等于null因为第一次进来,所以第一个条件成立,这里有一个非常重要的点,compareAndSetHead(new Node()),这里代表使用CAS算法设置head指向一个new Node(),所以new出来的这个Node对象的Thread一定等于null,因为没有对它进行设置,这就意味着,不论什么情况只要你有需要加入队列,那么队头一定会设置一个Thread=null的结点维护这个队列,所以这个队列的head就指向了一个Thread=null的Node结点,然后tail=head,这就让tail也指向这个结点所以就是下面这样:
在这里插入图片描述
因为是死循环,所以第二遍循环tail不再为空,已经指向了一个节点,所以会执行else,else才是真正把t2这个线程维护好放入结点,就是下面这样:
在这里插入图片描述

好的,至此一个没有抢到锁的线程就这样被加入队列,但是我们从代码发现,队列是加入了,但是线程似乎没有要停的意思,就是说我们还没有看到哪句代码让线程停下,ReentrantLock用的是自旋锁,我们看看在哪里体现的,addWaiter方法结束之后,acquireQueued会继续执行。

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

同样是一个死循环,node一样代表t2这个线程的Node结点,这个predecessor()函数拿到node的上一个节点,我们通过上面的图发现,node.predecessor()就是那个Thread=null的结点,p == head成立,这时候!!!!!!!!!!!!我们看到又tryAcquire(arg)了去尝试获取锁,为什么呢,因为如果你入队完成了,这时候t1也把锁释放了,那么你就不用再等待就直接去执行你的任务即可,这里我们假设如果tryAcquire(arg)成功了,那么它会将t2出队,然后去执行任务,第一个if里面的都是在t2出队后维护这个队列所以没什么,所以我们重点假设它还是没有tryAcquire(arg)成功,一定会执行第二个if,我们先来看看shouldParkAfterFailedAcquire。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            /*
             * This node has already set status asking a release
             * to signal it, so it can safely park.
             */
            return true;
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * waitStatus must be 0 or PROPAGATE.  Indicate that we
             * need a signal, but don't park yet.  Caller will need to
             * retry to make sure it cannot acquire before parking.
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

这里要注意,函数的两个参数,p是代表当前节点的上一个节点,node才是表示当前节点。
pred.waitStatus初始是0,所以ws初始为0,注意这里的ws,是pred这个节点的waitStatus的值,pred是上一个节点的就是那个Thread为空的节点,Node.SIGNAL是-1,这些都是Node类中定义的数值,很显然,会直接执行else,这时候会用CAS将pred.waitStatus设置为-1然后返回false,这里返回false就会导致又会返回acquireQueued方法中去继续死循环执行,又回去tryAcquire(arg),假设还是获取不到,进入这个方法,那么这时候第一个if条件就符合了,会返回true,接着会执行 parkAndCheckInterrupt()。

private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

好的看到这里,park了,也就是阻塞了这个线程,让它不能正常返回,到这里,t2被真正阻塞。

但是我们也发现了,t2在这里判断并改变waitStatus数值的时候,是改变的那个Thread=null的结点的waitStatus,代码是这么写的compareAndSetWaitStatus(pred, ws, Node.SIGNAL),并不是自己t2的waitStatus,也就是说,到底阻不阻塞,执不执行parkAndCheckInterrupt方法,是当前执行的线程去改变上一个节点的值决定的,如果t3来了加入了队列,那么它就要去改变t2的waitStatus去让自己自旋。将t2的waitStatus值为0然后变到-1,循环了两次,也就是自旋了两次,为什么是两次,我也说不清,可能大师经过测试两次最合适吧,两次拿不到锁,就阻塞,这应该是最合适的。

好的我们遗留了一个问题,hasQueuedPredecessors()这个方法,这个方法对线程的执行起了关键性的作用。

public final boolean hasQueuedPredecessors() {
        Node t = tail;
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
 }

这个方法只有简简单单几行代码,但是难的一匹,不知道大家是否记得,这个函数执行的条件必须是c==0,c是锁的状态,也就是锁处于自由状态的时候,才会去执行这个方法。这个方法的作用,就是判断你的队列中有没有已经早早就来了等待的线程,如果有,那么我当前线程就加入队列等待,如果没有,那么我去获取锁。

假设有t1,t2,t3三个线程,t1进来,锁一定是自由的,t1拿到了锁,进入这个方法,h一定等于t等于null,因为这时候队列里面没有任何元素,所以返回false,一路返回给t1加锁,这时候t2进来,t1还没有释放,所以不执行这个方法,t2直接被加入队列等待,这时候t3来了,t1恰好释放了锁,t3发现c==0,进入这个方法,这时候h!=t成立因为t2已经加入,队首和队尾一定不相同,然后(s = h.next) == null,这个是不成立的h.next是t2,所以不为null,s.thread != Thread.currentThread()成立因为s.thread是t2,Thread.currentThread()是t3,所以这个方法返回true,tryAcquire返回false,于是t3就要被加入队列。唤醒的就是t2,不论什么情况,只要队列中有等待的线程,这个方法都会获取到,这个函数就是保证先来后到的原则。这时候t2进来,t1还没有释放,所以不执行这个方法,t2直接被加入队列等待。

然后我们来说说它如何解锁,我们来看看unlock方法,就是如何进行解锁的

我们直接看release方法:

public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

同样它会先调用tryRelease(arg)方法尝试放弃锁。

protected final boolean tryRelease(int releases) {
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            setState(c);
            return free;
        }

tryRelease会先使锁的状态-1,此时c=0,然后设置拥有锁的线程为null,然后返回true,返回到release方法,此时我们队列不为null,所以h!=null成立,h.waitStatus != 0也成立因为没有人修改这个值,所以unparkSuccessor(h)这里面就是将锁释放。这里释放锁就结束了。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值