• 锁机制(三)


    public void java.util.concurrent.locks.ReentrantLock.lock()

    获取锁。

    如果该锁没有被另一个线程保持,则获取该锁并立即返回,将锁的保持计数设置为 1。

    如果当前线程已经保持该锁,则将保持计数加 1,并且该方法立即返回。

    如果该锁被另一个线程保持,则出于线程调度的目的,禁用当前线程,并且在获得锁之前,该线程将一直处于休眠状态,此时锁保持计数被设置为 1。

    从上面的文档可以看出ReentrantLock是可重入锁的实现。而内部是委托java.util.concurrent.locks.ReentrantLock.Sync.lock()实现的。java.util.concurrent.locks.ReentrantLock.Sync是抽象类,有java.util.concurrent.locks.ReentrantLock.FairSync和java.util.concurrent.locks.ReentrantLock.NonfairSync两个实现,也就是常说的公平锁和不公平锁。

    如果获取一个锁是按照请求的顺序得到的,那么就是公平锁,否则就是非公平锁。

    公平锁保证一个阻塞的线程最终能够获得锁,因为是有序的,所以总是可以按照请求的顺序获得锁。不公平锁意味着后请求锁的线程可能在其前面排列的休眠线程恢复前拿到锁,这样就有可能提高并发的性能。这是因为通常情况下挂起的线程重新开始与它真正开始运行,二者之间会产生严重的延时。因此非公平锁就可以利用这段时间完成操作。这是非公平锁在某些时候比公平锁性能要好的原因之一。

    本篇文章主要讲可重入锁中lock()的实现。可重入锁又分为公平锁和不公平锁两种实现。

    公平锁:直接调用AQS的acquire(int arg)

    非公平锁:

    final void lock() {
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1);
    }

    很显然,非公平锁在第一次获取锁,或者其它线程释放锁后(可能等待),优先采用compareAndSetState(0,1)然后设置AQS独占线程而持有锁,这样有时候比acquire(1)顺序检查锁持有而要高效。即使在重入锁上,也就是compareAndSetState(0,1)失败,但是是当前线程持有锁上,非公平锁也没有问题。

    在介绍实现之前先要补充上一节的知识,对于一个AQS的实现而言,通常情况下需要实现以下方法来描述如何锁定线程。

    • tryAcquire(int) 试图在独占模式下获取对象状态。此方法应该查询是否允许它在独占模式下获取对象状态,如果允许,则获取它。

      此方法总是由执行 acquire 的线程来调用。如果此方法报告失败,则 acquire 方法可以将线程加入队列(如果还没有将它加入队列),直到获得其他某个线程释放了该线程的信号。也就是说此方法是一种尝试性方法,如果成功获取锁那最好,如果没有成功也没有关系,直接返回false。

    • tryRelease(int) 试图设置状态来反映独占模式下的一个释放。 此方法总是由正在执行释放的线程调用。释放锁可能失败或者抛出异常,这个在后面会具体分析。
    • tryAcquireShared(int) 试图在共享模式下获取对象状态。
    • tryReleaseShared(int) 试图设置状态来反映共享模式下的一个释放。
    • isHeldExclusively() 如果对于当前(正调用的)线程,同步是以独占方式进行的,则返回 true

    除了tryAcquire(int)外,其它方法会在后面具体介绍。首先对于ReentrantLock而言,不管是公平锁还是非公平锁,都是独占锁,也就是说同时能够有一个线程持有锁。因此对于acquire(int arg)而言,arg==1。

    在AQS中acquire的实现如下:

    public final void acquire(int arg) 以独占模式获取对象,忽略中断。通过至少调用一次 tryAcquire(int) 来实现此方法,并在成功时返回。否则在成功之前,一直调用 tryAcquire(int) 将线程加入队列,线程可能重复被阻塞或不被阻塞。

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&                         //如果tryAcquire成功,就拿到锁,整个lock()过程结束
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //创建一个独占节点(Node)并且将此节点加入CHL队列末尾
    //
    自旋尝试获取锁,失败根据前一个节点来决定是否挂起(park()),直到成功获取到锁
         selfInterrupt(); //如果当前现场已经中断过,那么就中断当前线程(清除中断位) 
    }

    接下来所有篇幅都在分析上段中用到的4个方法:

    1.tryAcquire()

    2.addWaiter()

    3.acquireQueued()

    4.selfInterrupt()

    1.tryAcquire(acquires)

    对公平锁而言,其实现方式如下:

    在这段代码中,前面说明对于AQS存在一个state来描述当前有多少线程持有锁。由于AQS支持共享锁(例如读写锁,后面会继续讲),所以这里state>=0,但是由于ReentrantLock是独占锁,所以这里不妨理解为0<=state,acquires=1。isFirst(current)是一个很复杂的逻辑,包括踢出无用的节点等复杂过程,这里暂且不提,大体上的意思是说判断AQS是否为空或者当前线程是否在队列头(为了区分公平与非公平锁)。

        protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) { //c==0, 如果当前锁没有被其他线程持有
                if (isFirst(current) &&                 //如果当前线程在AQS队列头部,则尝试将state设为acquires(为1)
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current); //将AQS独占线程设为当前线程,并返回true
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {      //判断当前线程是否与独占线程相同,如果相同,将当前状态位加1
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);//修改状态位,并返回true
                return true;
            }
            return false;
        }
    }

    比较非公平锁的tryAcquire实现java.util.concurrent.locks.ReentrantLock.Sync.nonfairTryAcquire(int),公平锁多了一个判断当前节点是否在队列头,这个就保证了是否按照请求锁的顺序来决定获取锁的顺序(同一个线程的多次获取锁除外)。

    2.addWaiter(mode)

    tryAcquire失败就意味着入队列了。此时AQS的队列中节点Node就开始发挥作用了。一般情况下AQS支持独占锁和共享锁,而独占锁在Node中就意味着条件(Condition)队列为空(上一篇中介绍过相关概念)。在java.util.concurrent.locks.AbstractQueuedSynchronizer.Node中有两个常量,

    static final Node EXCLUSIVE = null; //独占节点模式

    static final Node SHARED = new Node(); //共享节点模式

    addWaiter(mode)中的mode就是节点模式,也就是共享锁还是独占锁模式。

    前面一再强调ReentrantLock是独占锁模式。

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

    当前仅当队列不为空并且将新节点插入尾部成功后直接返回新节点。否则进入enq(Node)进行操作。

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

    enq(Node)去队列操作实现了CHL队列的算法,如果为空就创建头结点,然后同时比较节点尾部是否是改变来决定CAS操作是否成功,当且仅当成功后才将为不节点的下一个节点指向为新节点。可以看到这里仍然是CAS操作。

    3.acquireQueued(node,arg)

    自旋请求锁,如果可能的话挂起线程,直到得到锁,返回当前线程是否中断过(如果park()过并且中断过的话有一个interrupted中断位)。

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

    一个节点是否该park()是关键,这是由方法java.util.concurrent.locks.AbstractQueuedSynchronizer.shouldParkAfterFailedAcquire(Node, Node)实现的。

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int s = pred.waitStatus;//1.如果前一个节点的等待状态waitStatus<0,也就是前面的节点还没有获得到锁,那么返回true,
        if (s < 0) return true;//表示当前节点(线程)就应该park()了
        if (s > 0) { //2.如果前一个节点的等待状态waitStatus>0,也就是前一个节点被CANCELLED了,那么就将前一个节点去掉
            do {
                node.prev = pred = pred.prev; //2.递归此操作直到所有前一个节点的waitStatus<=0,进行4。否则进行3。
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else compareAndSetWaitStatus(pred, 0, Node.SIGNAL);//3.前一个节点等待状态waitStatus=0,修改前一个节点状态位为SINGAL,表示后面有节点等待                                                          //处理,需要根据它的等待状态来决定是否该park()。进行4。
        return false;//4.返回false,表示线程不应该park()
    }

    4.selfInterrupt()

    private static void selfInterrupt() {
        Thread.currentThread().interrupt();
    }

    如果线程曾经中断过(或者阻塞过)(比如手动interrupt()或者超时等等,那么就再中断一次,中断两次的意思就是清除中断位)。

    大体上整个Lock.lock()就这样一个流程

  • 相关阅读:
    本博客主题的相关配置(2021年)
    vscode侧边栏隐藏不需要的文件
    虎扑,豆瓣等用css屏蔽广告代码
    代替pandownload的百度网盘下载软件
    网络请求的超时原因
    OkHttp3系列(三)okhttp3-fast-spring-boot-starter
    OkHttp3系列(二)MockWebServer使用
    OkHttp3系列(一)初识OkHttp3
    为什么要使用短链接
    Google Guava之简化异常和错误的传播与检查
  • 原文地址:https://www.cnblogs.com/jerrice/p/7286259.html
Copyright © 2020-2023  润新知