ReentrantLock与AbstractQueuedSynchronizer源码解析

在我们开发过程中,经常要和多线程打交道,多线程其实也是面试过程中必须问的问题,其实为什么会要使用多线程,这个大家可以百度下就清楚了。我们今天讲的这个ReentrantLock其实是对于同步(安全)的问题而产生的。大家应该都知道synchronized吧,他的作用业很简单,其实就是为了防止多线程下操作数据导致一些列问题,在1.5之前,其实都是基于这个synchronized的,而在jdk5的时候就推出了这个并发包,里面提供了与synchronized类似的工具,就是lock。

synchronized与lock区别

synchronized是java的一个关键字,synchronized是内置的语言实现,特点其实就是无需自己手动释放锁,它自己会释放的。他是阻塞的,也就是某个线程占用这个锁,其他的锁只能等待他释放。线程对于锁全部为抢占方式。
lock:lock锁最大的好处就是方便,你可以用于任何你想要使用的地方,他非常灵活,它里面也分为公平锁和非公平锁,还有他不像synchronized一直阻塞,他里面提供了一系列的方法比如(trylock方法)。他的底层都是通过cas来进行修改数据的。


在讲解ReentrantLock之前,我们必须要了解一下他的构成,我们之前一直听别人说有个叫aqs的东西,这个东西也是并发包的,其实这个aqs就是AbstractQueuedSynchronizer,他是我们并发包中的一个核心,我们的ReentrantLock以及其他类都是继承他的,他里面提供了一系列的方法,他内部实现主要是维护了一个双链表,双链表的目的其实就是为了公平锁,也就是先来先到,主要是使用一个violate修饰的state来表示锁的状态,
这里写图片描述
这个双链表里面维护了四个东西,prev和next就不用说了吧,而waitstatus变量表示的是当前的线程的状态,他有以下几个状态CANCELLED、SIGNAL、CONDITION、PROPAGATE等值,改变量的作用主要是为了判断该几点的状态。然后每次线程释放锁都会进行唤醒通知,他的唤醒通知不想notify随机唤醒,他是沿着链表来唤醒的,比如根节点释放锁的时候,他会去通知他的next节点,然后next节点就会去尝试获取锁。 而我们的ReentrantLock也是基于这个实现的,虽然他里面分为公平锁和非公平锁,其实原理都是一样的,ReentrantLock是通过内部类sync来实现我们的aqs的方法,如下图所示
这里写图片描述

而我们的源码部分主要是通过获取锁、加入队列(这边指的是双链表)、判断状态、释放锁、唤醒线程。这几部分一一讲解。主要是以公平锁来讲。
首先获取锁,也就是lock方法

  final void lock() {
  //其实这个就是调用acquire方法,这个在我们的aqs里面是有实现的
            acquire(1);
        }

我们直接去aqs里面看这个方法

public final void acquire(int arg) {
//首先会尝试获取锁,如果获取失败,则将我们线程加入到双链表中
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

到这里,我们要看一下的几个方法,第一个就是tryAcquire方法

   protected final boolean tryAcquire(int acquires) {
   //获取当前的线程
            final Thread current = Thread.currentThread();
            //获取锁的状态,我们之前说过,aqs是通过维护state来表示锁的状态,这个锁是可见的,也就是使用了voilate来修饰的
            int c = getState();
            //如果值为0的话代表是没有人占用锁,,然后通过cas去修改state,并返回true,代表获取成功
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            //我们知道我们的锁是可重入的,这个和sync是一样的,所以如果当前的线程已经获取到锁,那么他就可以进入其他需要的锁的地方,这时候在将锁的状态+1
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            //如果都不是,则直接放回false,证明已经被占用了
            return false;
        }
    }

这时候我们就需要执行下面的代码,也就是将我们的线程加入链表中。也就是这个方法addWaiter

    */
    private Node addWaiter(Node mode) {
    //先new一个节点,将线程放入该节点
        Node node = new Node(Thread.currentThread(), mode);
        //开始的时候tail也是为null的
        Node pred = tail;
        if (pred != null) {
        //不为null的话,就直接将我们的节点直接放在后面
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        //所以直接进入这个方法
        enq(node);
        //返回node节点
        return node;
    }

我们继续看enq(node)方法

  private Node enq(final Node node) {
  //这边使用了一种死循环,直到成功为止
        for (;;) {
            Node t = tail;
            //如果为null的话就新建一个节点,然后将该节点赋值给tail,那么下一次执行的时候,就有值了
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
            //这时候就会将我们的这个节点加入到tail的后面,然后结束循环,这边也使用 了cas来处理,因为可能有多个线程都要加入链表
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
    }

到这里我们的加入链表也完成了,可是还有个就是我们之前说过aqs里面是有个waitstatus的状态的,这个在哪呢,这个就是在我们这个addwiater方法前有个acquireQueued这个方法,这个方法的作用就是去设置我们这个等待状态。

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            //也是一个死循环,其实标准叫法叫自旋
            for (;;) {
            //拿到该节点的前驱
                final Node p = node.predecessor();
                //如果该节点的前驱是head的话,那么久去尝试获取锁
                if (p == head && tryAcquire(arg)) {
                //获取成功的话,就将当前节点置为头节点
                    setHead(node);
                    //然后释放之前的head节点,避免无线增长链表的长度。然后返回
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //如果不是的话,那么就看自己是否可以休息
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

我们要继续去看这个方法shouldParkAfterFailedAcquire

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    //我们这边通过传进来的前置节点和当前节点来,首先获取前置节点的waitstatus,这个之前说过总共有四种状态,每种状态表示的是不同意思
        int ws = pred.waitStatus;
        //如果前置节点的状态为SIGNAL的话,那么直接返回true
        if (ws == Node.SIGNAL)
            return true;
            //如果大于0,其实也就是cancel,代表前置节点已经取消排队了,所以我们要将前置节点给剔除掉。
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {//然后设置前置节点的状态为SIGNAL,最后返回false
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

如果返回的是true的话,那么久会继续执行这个方法parkAndCheckInterrupt()

 private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        //判断当前线程是否被中断,被中断的话,也就意味着他可以从队列中进行移除,这时候就会调用finally的方法,也就是cancelAcquire(node);
        return Thread.interrupted();
    }

它是通过LockSupport.park(this)将当前线程挂起到WATING状态,它需要等待一个中断、unpark方法来唤醒它,通过这样一种FIFO的机制的等待,来实现了Lock的操作,如果我们的线程中途被打断了,那么
我们就会执行cancelAcquire(node)方法,这个方法在finally中

 private void cancelAcquire(Node node) {
 //如果当前节点为null,
        if (node == null)
            return;
            //我们自己手动将当前节点的线程置为null,
        node.thread = null;
        //找到该节点的前置节点
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;
//将当前节点的wait状态置为cancel
        node.waitStatus = Node.CANCELLED;

        if (node == tail && compareAndSetTail(node, pred)) {
            compareAndSetNext(pred, predNext, null);
        } else {
            int ws;
            if (pred != head &&
                ((ws = pred.waitStatus) == Node.SIGNAL ||
                 (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
                pred.thread != null) {
                Node next = node.next;
                if (next != null && next.waitStatus <= 0)
                    compareAndSetNext(pred, predNext, next);
            } else {
                unparkSuccessor(node);
            }

            node.next = node; // help GC
        }
    }

至此我们如何获取锁的源码都解释了一篇,其实大致的过程可以分为以下几步
1、线程使用lock的方法获取锁的时候,会尝试去获取锁,如果获取成功的话就直接返回并修改锁的状态。如果发现当前线程已经持有锁的时候,还可以继续获取锁(重入锁),这时候就讲锁的状态+1.
2、如果线程获取锁失败,也就是锁的状态不为0,这时候就要将我们的线程放入到双链表中,放入链表的步骤,首先是判断tail是否为空,如果当前的线程刚好是第一个的时候,这时候我们就会new一个节点,然后将我们的这个线程放到我们new出来的节点后面(这边采用自旋的方式来加入链表,因为公平就会涉及很多线程获取不到锁,那么他们就都会加入链表中)。
3、线程加入链表的操作完成之后,我们还要考虑有些获取锁的线程是尝试去获取,也就是达到一定时间之后没有获取到,他就自我打断,不参加排队,这个时候我们就要去检查状态,看哪些是可以从链表移除,哪些是可以继续休息的,这时候就要去看我们链表中节点中维护的waitstatus的变量值,首先会去判断当前的节点的前置几点是否我head,如果是的话就尝试去获取锁,并将当前节点设置为head,并释放前置节点。
4、如果不是的话,那么就会去执行判断自己是否可以wait,这时候就是去判断waitstatus的值,如果前置节点的值为signal的话就直接返回true,如果不是的话就判断状态是否大于0,大于0表示该线程已经取消了,可以将他丢弃。如果都不是就将前置节点设置为signal,记住这边也是用了自旋方式。
5、如果当前节点的前置节点已经是signal的话,那么就将当前节点放入waiting状态,并返回他的打断状态。
6、如果被打断,那么我们就继续执行清空当前线程的节点。
至此我们如何获取锁以及状态怎么控制都讲解完了,下面我们会讲一下如何去释放锁以及如何唤醒下一个节点的线程。
首先我们直接去找unlock的方法,其实这个方法调用就是aqs的底层方法

   public void unlock() {
   //底层是调用aqs的release
        sync.release(1);
    }

release方法

public final boolean release(int arg) {
//首先尝试去释放锁,这个方法aqs没有帮我们实现,需要我们自己去重写
        if (tryRelease(arg)) {
        //释放锁成功,这时候就要去链表拿去头部,
            Node h = head;
            //判断链表的头部节点是否为null,并且他的状态是够为0,因为我们之前加入链表就将没必要的都清空,留下的应该都是signal
            if (h != null && h.waitStatus != 0)
            //这时候就通知head节点去获取锁
                unparkSuccessor(h);
            return true;
        }
        return false;
    }

我们去看一下这个方法tryRelease,这个在ReentrantLock有实现

protected final boolean tryRelease(int releases) {
//首先会讲当前状态锁-1,主要有重入锁的存在
            int c = getState() - releases;
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            //如果锁为0,表示释放成功,这个时候就需要将自己的线程设置为null,同时将free设置为true
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            //然后改变锁的状态,也就是恢复没有被占用的状态,然后返回true
            setState(c);
            return free;
        }

我们看一下他释放锁成功后,执行的通知head节点去获取锁的状态,unparkSuccessor

 private void unparkSuccessor(Node node) {
 //先获取当前头结点的转态
        int ws = node.waitStatus;
        //如果小于0的话,那么就将值给置为0,
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

   //然后在去找他下一个节点
        Node s = node.next;
        //如果下个节点的值为null,或者下个节点的状态为1的话,那么就直接将当前节点给置为null,然后继续去找下个节点,在释放下个节点
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
            LockSupport.unpark(s.thread);
    }

到此为止我们释放锁的方法也讲完了,其实不是很难,只要你跟着这个逻辑走其实不是很难,其实我们发现其实这个ReentrantLock源码中好多是直接调用aqs的,其实aqs的源码大致也就是这样的,只是有些东西需要我们通过自己的实现方式来实现的时候,就需要去覆盖掉他。aqs就是父类,假如我们自己想实现类似这样的锁的时候,也要去继承这个父类aqs,理解了aqs的这些方法,基本上这个并发报的好多东西也直接明白了,我们本篇讲的是公平锁,非公平锁也类似,大家可以根据这篇博客去看看。


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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值