ReentrantLock中lock与unlock源码详解

   文中表格出处https://www.cnblogs.com/xrq730/p/4979021.html

   文中图片出处https://blog.csdn.net/luonanqin/article/details/41871909

    对于java中实现并发加锁的方式可以分为两种,一种是重量级的synchronized,一种是concurrent包下的lock接口。本系列文章将对这两种锁和依赖于cpu嗅探技术的volatile进行详细说明。

    在Lock接口出现前,Java程序是靠synchronized的关键字实现锁功能的,而Java SE 5 之后,并发包中新增了Lock接口及其相关实现类用来实现锁功能,它提供了与synchronized关键字类似的同步功能,知识在使用时需要显示的获取和释放锁。虽然它缺少了隐式获取释放锁的便捷性,但是拥有了锁获取与释放的可操作性,可中断的获取锁以及超时获取锁等synchronized关键字所不具备的同步特性。

    使用范例:这里使用的是lock的实现类Reentrantlock

private static Lock lock = new ReentrantLock();

public static void critical() {

    lock.lock();

    try {

        System.out.println("start " + Thread.currentThread().getName());

        Thread.sleep(1000);

        System.out.println("end " + Thread.currentThread().getName());

    } catch (InterruptedException e) {

        e.printStackTrace();

    } finally {

        lock.unlock();

    }

}

     注意:不能在try块中进行lock,如果代码发生异常,会导致锁的无故释放

    在介绍Reentrantlock前首先需要介绍一下AbstractQueuedSynchronizer,下面简称AQS。它是一个非常重要的同步队列,concurrent包下许多工具都用到了这个队列同步器,比如著名的CountDownLatch。通过查看源码可以发现,AQS在内部实现了一个链式FIFO的双向队列,拥有Head和Tail指针,并且实现了setHead(),setTail()等队列方法。既然是队列,那势必有结点,AQS就是通过对一个个结点的管理实现同步作用的,当当前线程获取锁失败的时候,AQS将当前线程的状态包装为一个结点,放入内部的queue中并阻塞当前线程。当可以再次获取锁的时候,AQS将首结点对应的线程唤醒并再次尝试获取锁。

    下面用一张表说明结点内的属性。

属    性

定    义

Node SHARED = new Node()

表示Node处于共享模式

Node EXCLUSIVE = null

表示Node处于独占模式

int CANCELLED = 1

因为超时或者中断,Node被设置为取消状态,被取消的Node不应该去竞争锁,只能保持取消状态不变,不能转换为其他状态,处于这种状态的Node会被踢出队列,被GC回收

int SIGNAL = -1

表示这个Node的继任Node被阻塞了,到时需要通知它

 int CONDITION = -2

表示这个Node在条件队列中,因为等待某个条件而被阻塞 

int PROPAGATE = -3

使用在共享模式头Node有可能处于这种状态, 表示锁的下一次获取可以无条件传播

 int waitStatus

0,新Node会处于这种状态 

 Node prev

队列中某个Node的前驱Node 

 Node next

队列中某个Node的后继Node 

Thread thread

这个Node持有的线程,表示等待锁的线程

Node nextWaiter

表示下一个等待condition的Node

    

    再用一张表说明AQS的属性和方法

属性/方法

含    义

Thread exclusiveOwnerThread

这个是AQS父类AbstractOwnableSynchronizer的属性,表示独占模式同步器的当前拥有者

Node

上面已经介绍过了,FIFO队列的基本单位

Node head

FIFO队列中的头Node

Node tail

FIFO队列中的尾Node

int state

同步状态,0表示未锁

int getState()

获取同步状态

setState(int newState)

设置同步状态

boolean compareAndSetState(int expect, int update) 

利用CAS进行State的设置 

 long spinForTimeoutThreshold = 1000L

线程自旋等待的时间 

Node enq(final Node node) 

插入一个Node到FIFO队列中 

Node addWaiter(Node mode)

为当前线程和指定模式创建并扩充一个等待队列

void setHead(Node node)

设置队列的头Node

void unparkSuccessor(Node node)

如果存在的话,唤起Node持有的线程

void doReleaseShared()

共享模式下做释放锁的动作

void cancelAcquire(Node node)

取消正在进行的Node获取锁的尝试

boolean shouldParkAfterFailedAcquire(Node pred, Node node)

在尝试获取锁失败后是否应该禁用当前线程并等待

void selfInterrupt()

中断当前线程本身

boolean parkAndCheckInterrupt()

禁用当前线程进入等待状态并中断线程本身

boolean acquireQueued(final Node node, int arg)

队列中的线程获取锁

tryAcquire(int arg)

尝试获得锁(由AQS的子类实现它

tryRelease(int arg)

尝试释放锁(由AQS的子类实现它

isHeldExclusively()

是否独自持有锁

acquire(int arg)

获取锁

release(int arg)

释放锁

compareAndSetHead(Node update)

利用CAS设置头Node

compareAndSetTail(Node expect, Node update)

利用CAS设置尾Node

compareAndSetWaitStatus(Node node, int expect, int update)

利用CAS设置某个Node中的等待状态

    上表中有很多的CAS操作,这里解释一下CAS,全称为CompareAndSwap,即比较并交换。顾名思义,当需要更新值的时候,先用当前值和旧值进行比较,如果相同则更新,不同则返回一个错误码。它是concurrent存在的基础,CAS的实现并不是通过代码,而是通过CPU对其的支持,不同的CPU对CAS的实现方式不同,但是最后的效果是相同的,当需要触发CAS的时候,通过调用Java的Unsafe类对硬件级别进行操作,从而实现比较并交换这一操作,Unsafe类是Java自己留的后门,从而实现不依赖native方法就可以实现硬件级别的原子操作。

    Reentrantlock中的锁分为两种,公平锁和非公平锁,通过new Reentrantlock(true)可以获得公平锁,默认是非公平锁。对于公平锁,每次有线程到达时,都会先通过hasQueuedPredecessors()方法查看等待队列是否为空,不为空则加入等待队列。而非公平锁则不然,可以通过查看下面我的源码分析看出,非公平锁回多次尝试对锁的所有权进行抢占,全部失败之后,才会按公平锁的方式完全加入等待队列。至于为什么要用非公平锁,自然是因为它快啊,这一点在源码的注释中也写了。一个形象的比喻就是打官司是私了肯定比上法庭快,免去了线程挂起的时候进行线程切换的时间。

    Reentrantlock是一种可重入锁,也就是一个线程可以获取锁多次。操作系统在切换线程的时候,是需要保存并切换上下文的,这也就是为什么在单核的cpu上进行多线程操作时反而更加费时的原因。在可重入锁中当前线程可以直接进行重入,这样就不会存在上下文切换的时间,从而提高了效率。下文介绍的lock和unlock都是针对非公平锁的。

1.lock

    首先,我们假设线程1先占有了锁,这个过程是非常简单的

    (1)首先,调用

private final Sync sync;

public ReentrantLock(boolean fair) {

    sync = fair ? new FairSync() : new NonfairSync();

}

实例化出一个非公平锁,其中Sync是一个继承了AQS的类。

    (2)调用sync的lock方法

final void lock() {

    if (compareAndSetState(0, 1))

        setExclusiveOwnerThread(Thread.currentThread());

    else

        acquire(1);

}

    (3)调用AQS的compareAndSetState方法,将state属性设为1,表示线程1独占锁

protected final boolean compareAndSetState(int expect, int update) {

    // See below for intrinsics setup to support this

    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);

}

    (4)调用AQS父类AOS的setExclusiveOwnerThread方法将占有锁的线程设为当前线程

protected final void setExclusiveOwnerThread(Thread thread) {

    exclusiveOwnerThread = thread;

}

    此时线程1成功占有锁,此时AQS中的state=1,exclusiveOwnerThread = thread1

    线程1占有锁之后,线程2到来,由于是非公平锁,线程2从到来直到加入队列的过程中,它会多次尝试抢占锁,如果均不成功,最后会阻塞并被加入到AQS的等待队列中,下图是调用链。

(1)调用lock()方法

final void lock() {

    if (compareAndSetState(0, 1))

        setExclusiveOwnerThread(Thread.currentThread());

    else

        acquire(1);

}

    在调用lock时,首先会尝试使用CAS判断state是不是0,如果是0就设为1。这里是非公平的一个体现,新到线程可以去抢占队首节点的锁的所有权。但是现在这里是失败的,因为线程1已经把state设置成了1。所以跳到第二步

(2)调用AQS的acquire方法

public final void acquire(int arg) {

    if (!tryAcquire(arg) &&

        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

        selfInterrupt();

}

    可以看到这个if语句,之前说过,非公平锁从开始到放入等待队列结束期间,会多次尝试获得锁,这里的tryAcquire()就是尝试获得锁,非公平锁中它最后会走nonfairTryAcquire方法

protected final boolean tryAcquire(int acquires) {

    return nonfairTryAcquire(acquires);

}



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;

}

    首先获取到当前线程,获取当前占有锁的线程数,如果state等于0说明锁没有被占用。此时尝试用CAS将state设置为1,如果失败,再判断占有锁的是不是当前线程。如果是,走else if里的代码,则开始重入,这里是对锁的重入次数做了一个限制,我们知道,整型的数如果超过范围会溢出为负数,所以当锁被当前线程重入时,每次都会对state加一,如果state变成负数,那么说明重入次数已经超过2147483647次,此时抛出异常。很简单的就实现了一个偏向锁。

    如果acquire失败,走第二个条件尝试将当前线程加入等待队列中。首先走addWaiter方法将当前线程的信息包装成node并加入队尾

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;

}

    首先先创建一个节点,因为是独占锁,所以实际上传进来的mode是个NULL,此时new Node会创建一个独占模式的节点。此时先获取一下尾节点,如果尾节点都是null那么说明这个队列就是空,此时走enq方法新建一个队列。如果存在,用CAS把它设为队列的头节点,用CAS的原因是cpu时间片是切换的,可能有其他线程把队列已经创建好了,此时CAS失败,由于tail是volatile的,所以对当前线程可见,此时看见tail不为null再次循环后将会将当前节点放到队尾。

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中这里用了一个for的死循环保证队列是一定新建成功的,它会先再次判断一下队列是不是不存在,然后新建头节点,然后把当前节点连在头节点后面。并将当前节点设为尾节点。这里其实是这样的,

    到这里算是把一个就绪的节点放入AQS的等待队列中了,开始走acquireQueued(addWaiter(Node.EXCLUSIVE), arg))最外层的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);

    }

}

    这里第一个if语句是对当前节点能否获得锁进行判断,因为此时可能锁已经被释放。如果当前节点是队列中的第一个节点而且能够获得锁,那么就把当前节点设置为头节点。如果还是不能获取锁,那么需要对这条线程进行阻塞。但因为并不是所有线程都需要阻塞,比如取消状态。这里开始走shouldParkAfterFailedAcquire(p, node)方法判断是否需要阻塞。

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;

}

    因为是多线程环境,当前节点的位置是有可能变得,所以这里需要再判断一下status,这里根据status设定三个规则

    规则1:如果前继的节点状态为SIGNAL,表明当前节点需要unpark,则返回成功,此时acquireQueued方法的第12行(parkAndCheckInterrupt)将导致线程阻塞

    规则2:如果前继节点状态为CANCELLED(ws>0),说明前置节点已经被放弃,则回溯到一个非取消的前继节点,返回false,acquireQueued方法的无限循环将递归调用该方法,直至规则1返回true,导致线程阻塞

    规则3:如果前继节点状态为非SIGNAL、非CANCELLED,则设置前继的状态为SIGNAL,返回false后进入acquireQueued的无限循环,与规则2同

    这里如果返回true,说明线程需要被阻塞,那么通过parkAndCheckInterrupt调用native方法进行阻塞。

private final boolean parkAndCheckInterrupt() {

    LockSupport.park(this);

    return Thread.interrupted();

}

public static void park(Object blocker) {

    Thread t = Thread.currentThread();

    setBlocker(t, blocker);

    UNSAFE.park(false, 0L);

    setBlocker(t, null);

}

    如果在acquireQueued方法中出现了异常,此时failed值为true,会走finally块中的cancelAcquire方法把当前节点设为CANCELLED状态

private void cancelAcquire(Node node) {

    // Ignore if node doesn't exist

    if (node == null)

        return;

    node.thread = null;

    Node pred = node.prev;

    while (pred.waitStatus > 0)

        node.prev = pred = pred.prev;

    Node predNext = pred.next;

    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

    }

}

    在cancelAcquire方法中,首先将node.thread设为null,表示此节点不关联任何线程。然后通过while找到当前节点前面第一个不是CANCELLED状态的节点,并且将当前节点的status设置为CANCELLED。因为头节点一定不是CANCELLED状态,所以这里是一定能够找到的,新增一个predNext便于下面CAS操作。然后接下来分三种情况

    规则1:如果当前节点是尾节点,那么将当前节点的前一个节点为尾节点,并将其next设为null,注意这里并没有设置pre为null,后面unlock的时候会用到。

    规则2:如果当前节点不是尾节点也不是头节点的后继节点,那么判断其前驱节点的status是不是SIGNAL,如果不是那么尝试将其设为SIGNAL。这两个条件满足一个之后继续判断前驱节点是否与线程关联,如果这些都满足,那么将当前节点的前驱节点和当前节点的后继节点相连,相当于将当前节点从队列删除。

    规则3:如果当前节点是头节点的后继节点,那么调用unparkSuccessor唤醒当前节点的后继节点。

    最后把next指向自身,减少引用达到help GC的目的。

    这里用一张网上的图总结一下lock的流程

    至此,lock过程结束。接下来看unlock方法,首先肯定是调用unlock方法,我们可以看到unlock里调用release方法

public void unlock() {

    sync.release(1);

}

public final boolean release(int arg) {

    if (tryRelease(arg)) {

        Node h = head;

        if (h != null && h.waitStatus != 0)

            unparkSuccessor(h);

        return true;

    }

    return false;

}

    这里先走外层的if语句,这里进入tryRelease方法,arg = 1

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;

}

    首先获取计算锁释放之后的state值,用于后续是否释放成功的判断。这里会先对要释放的锁的线程是不是当前线程做判断,如果不是直接抛出异常。如果是当前线程,再看刚才计算的state是不是0。因为有重入锁的概念,每次拥有锁state会加一,释放锁会减一。这里如果state为0说明当前线程将锁完全释放,此时可以用setExclusiveOwnerThread把当前锁的拥有者设为null。如果不是0那就把state设置为state-1。

  此时进入release的if方法体内,这里对头节点的状态进行了判断,由于之前对waitStatus的设置,现在队列里除了最后一个节点之外应该所有节点的waitStatus都是SIGNAL也就是-1。所以走unparkSuccessor方法。

private void unparkSuccessor(Node node) {

    /*

     * If status is negative (i.e., possibly needing signal) try

     * to clear in anticipation of signalling.  It is OK if this

     * fails or if status is changed by waiting thread.

     */

    int ws = node.waitStatus;

    if (ws < 0)

        compareAndSetWaitStatus(node, ws, 0);

    /*

     * Thread to unpark is held in successor, which is normally

     * just the next node.  But if cancelled or apparently null,

     * traverse backwards from tail to find the actual

     * non-cancelled successor.

     */

    Node s = node.next;

    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);

}

    在unparkSuccessor方法里首先会获取node的waitStatus,首先会把当前节点的值waitStatus设为0,也就是新节点,前提当前节点的waitStatus是SIGNAL。然后获取当前节点的后继节点,这里还是要判断一下是否为空和是否为CANCELLED。如果是的话,它会从队尾到队首向前遍历,直到找到最靠近头节点的那个可以被唤醒的节点,然后把它赋值给s。这里有个为什么是从后往前找的问题,这样做有两个原因

    1.我们可以回忆一下之前往队列里加node时候自旋操作的代码。

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;

            }

        }

    }

}

    我们可以看到这里用CAS设置了头节点,这是可以保证线程安全的,但是在CAS操作和t.next = node这一步之间,如果有别的线程进行释放锁,这时候next还没设置完,从前往后遍历是无法访问到最后一个节点的。

    2.之所以要遍历队列就是因为出现了CANCELLED状态的节点,在之前调用cancelAcquire方法将节点设为CANCELLED状态时,最后为了help GC将node.next指向了自己,这时候从前往后遍历,势必会造成死循环。

    第一个if方法走完之后,调用LockSupport.unpark方法,这个方法通过调用native方法唤醒后面的线程。至此unlock结束。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值