• java多线程之ReentrantLock


    前言

    相信学过java的人都知道 synchronized 这个关键词,也知道它用于控制多线程对并发资源的安全访问,兴许,你还用过Lock相关的功能,但你可能从来没有想过java中的锁底层的机制是怎么实现的。如果真是这样,而且你有兴趣了解,今天我将带领你轻松的学习下java中非常重要,也非常基础的可重入锁-ReentrantLock的实现机制。

    ReentrantLock是一个可重入的互斥锁,又被称为“独占锁”。

    顾名思义,ReentrantLock锁在同一个时间点只能被一个线程锁持有;而可重入的意思是,ReentrantLock锁,可以被单个线程多次获取。
    ReentrantLock分为“公平锁”和“非公平锁”。它们的区别体现在获取锁的机制上是否公平。“锁”是为了保护竞争资源,防止多个线程同时操作线程而出错,ReentrantLock在同一个时间点只能被一个线程获取(当某线程获取到“锁”时,其它线程就必须等待);ReentraantLock是通过一个FIFO的等待队列来管理获取该锁所有线程的。在“公平锁”的机制下,线程依次排队获取锁;而“非公平锁”在锁是可获取状态时,不管自己是不是在队列的开头都会获取锁。


    听故事把知识掌握了

    在一个村子里面,有一口井水,水质非常的好,村民们都想打井里的水。这井只有一口,村里的人那么多,所以得出个打水的规则才行。村长绞尽脑汁,最终想出了一个比较合理的方案,咱们来仔细的看看聪明的村长大人的智慧。

    井边安排一个看井人,维护打水的秩序。

    打水时,以家庭为单位,哪个家庭任何人先到井边,就可以先打水,而且如果一个家庭占到了打水权,其家人这时候过来打水不用排队。而那些没有抢占到打水权的人,一个一个挨着在井边排成一队,先到的排在前面。打水示意图如下 :

    打水示意图

    是不是感觉很和谐,如果打水的人打完了,他会跟看井人报告,看井人会让第二个人接着打水。这样大家总都能够打到水。是不是看起来挺公平的,先到的人先打水,当然不是绝对公平的,自己看看下面这个场景 :

    同家人一起打水

    看着,一个有娃的父亲正在打水,他的娃也到井边了,所以女凭父贵直接排到最前面打水,羡煞旁人了。 
    以上这个故事模型就是所谓的公平锁模型,当一个人想到井边打水,而现在打水的人又不是自家人,这时候就得乖乖在队列后面排队。

    事情总不是那么一帆风顺的,总会有些人想走捷径,话说看井人年纪大了,有时候,眼力不是很好,这时候,人们开始打起了新主意。新来打水的人,他们看到有人排队打水的时候,他们不会那么乖巧的就排到最后面去排队,反之,他们会看看现在有没有人正在打水,如果有人在打水,没辄了,只好排到队列最后面,但如果这时候前面打水的人刚刚打完水,正在交接中,排在队头的人还没有完成交接工作,这时候,新来的人可以尝试抢打水权,如果抢到了,呵呵,其他人也只能睁一只眼闭一只眼,因为大家都默认这个规则了。这就是所谓的非公平锁模型。新来的人不一定总得乖乖排队,这也就造成了原来队列中排队的人可能要等很久很久。 

    java可重入锁-ReentrantLock实现细节

    ReentrantLock支持两种获取锁的方式,一种是公平模型,一种是非公平模型。在继续之前,咱们先把故事元素转换为程序元素。

    元素转换 

    咱们先来说说公平锁模型:

    初始化时, state=0,表示无人抢占了打水权。这时候,村民A来打水(A线程请求锁),占了打水权,把state+1,如下所示:

    线程A获取锁

    线程A取得了锁,把 state原子性+1,这时候state被改为1,A线程继续执行其他任务,然后来了村民B也想打水(线程B请求锁),线程B无法获取锁,生成节点进行排队,如下图所示:

    线程B等待

    初始化的时候,会生成一个空的头节点,然后才是B线程节点,这时候,如果线程A又请求锁,是否需要排队?答案当然是否定的,否则就直接死锁了。当A再次请求锁,就相当于是打水期间,同一家人也来打水了,是有特权的,这时候的状态如下图所示:

    可重入锁获取

    到了这里,相信大家应该明白了什么是可重入锁了吧。就是一个线程在获取了锁之后,再次去获取了同一个锁,这时候仅仅是把状态值进行累加。如果线程A释放了一次锁,就成这样了:

    线程A释放一次锁

    仅仅是把状态值减了,只有线程A把此锁全部释放了,状态值减到0了,其他线程才有机会获取锁。当A把锁完全释放后,state恢复为0,然后会通知队列唤醒B线程节点,使B可以再次竞争锁。当然,如果B线程后面还有C线程,C线程继续休眠,除非B执行完了,通知了C线程。注意,当一个线程节点被唤醒然后取得了锁,对应节点会从队列中删除。 

    非公平锁模型

    如果你已经明白了前面讲的公平锁模型,那么非公平锁模型也就非常容易理解了。当线程A执行完之后,要唤醒线程B是需要时间的,而且线程B醒来后还要再次竞争锁,所以如果在切换过程当中,来了一个线程C,那么线程C是有可能获取到锁的,如果C获取到了锁,B就只能继续乖乖休眠了。这里就不再画图说明了。

     队列是非阻塞的。

    获取锁的步骤:

     公平锁

      若“锁没有被任何线程锁拥有”,则判断“当前线程”是不是CLH队列中的第一个线程线程(队列中没有线程了),若是的话,则获取该锁,设置锁的状态,并切设置锁的拥有者为“当前线程”,如果不是第一个线程,则加入到队列中

       如果如果“锁”的持有者已经是当前线程,则将更新锁的状态(+1)即可(也就是重入);

      如果不属于上面的两种情况(被其他线程拥有),则将该线程加入队列等待。

       非公平锁

          1 如果“锁”没有被任何线程拥有,则通过CAS函数设置“锁”的状态,同时,设置“当前线程”为锁的持有者,然后返回true。
         2 如果“锁”的持有者已经是当前线程,则将更新锁的状态(+1)即可(也就是重入)。
         3  如果不属于上面的两种情况,则将该线程加入队列等待。

    释放锁

    如果“当前线程”不是“锁的持有者”,则抛出异常。

    每释放锁一次,锁的状态-1。
     如果“当前线程”在本次释放锁操作之后,对锁的拥有状态是0(即,当前线程彻底释放该“锁”),则设置“锁”的持有者为null,即锁是可获取状态。同时,更新当前线程的锁的状态为0。

    如果当前线程释放成功了,state=0,会唤醒后继线程(从队列中)获取锁。

    上半部分重点理解,下面对部分源码进行解析

    函数列表
    // 创建一个 ReentrantLock ,默认是“非公平锁”。
    ReentrantLock()
    // 创建策略是fair的 ReentrantLock。fair为true表示是公平锁,fair为false表示是非公平锁。
    ReentrantLock(boolean fair)
    
    // 查询当前线程保持此锁的次数。
    int getHoldCount()
    // 返回目前拥有此锁的线程,如果此锁不被任何线程拥有,则返回 null。
    protected Thread getOwner()
    // 返回一个 collection,它包含可能正等待获取此锁的线程。
    protected Collection<Thread> getQueuedThreads()
    // 返回正等待获取此锁的线程估计数。
    int getQueueLength()
    // 返回一个 collection,它包含可能正在等待与此锁相关给定条件的那些线程。
    protected Collection<Thread> getWaitingThreads(Condition condition)
    // 返回等待与此锁相关的给定条件的线程估计数。
    int getWaitQueueLength(Condition condition)
    // 查询给定线程是否正在等待获取此锁。
    boolean hasQueuedThread(Thread thread)
    // 查询是否有些线程正在等待获取此锁。
    boolean hasQueuedThreads()
    // 查询是否有些线程正在等待与此锁有关的给定条件。
    boolean hasWaiters(Condition condition)
    // 如果是“公平锁”返回true,否则返回false。
    boolean isFair()
    // 查询当前线程是否保持此锁。
    boolean isHeldByCurrentThread()
    // 查询此锁是否由任意线程保持。
    boolean isLocked()
    // 获取锁。
    void lock()
    // 如果当前线程未被中断,则获取锁。
    void lockInterruptibly()
    // 返回用来与此 Lock 实例一起使用的 Condition 实例。
    Condition newCondition()
    // 仅在调用时锁未被另一个线程保持的情况下,才获取该锁。
    boolean tryLock()
    // 如果锁在给定等待时间内没有被另一个线程保持,且当前线程未被中断,则获取该锁。
    boolean tryLock(long timeout, TimeUnit unit)
    // 试图释放此锁。
    void unlock()

    1、lock()方法的实现 
    进入lock()方法,发现其内部调用的是sync.lock();

      public void lock() {
            sync.lock();
        }

    sync是在ReentrantLock的构造函数中实现的。其中fair参数的不同可实现公平锁和非公平锁。由于在锁释放的阶段,锁处于无线程占有的状态,此时其他线程和在队列中等待的线程都可以抢占该锁,从而出现公平锁和非公平锁的区别。 
    非公平锁:当锁处于无线程占有的状态,此时其他线程和在队列中等待的线程都可以抢占该锁。 
    公平锁:当锁处于无线程占有的状态,在其他线程抢占该锁的时候,都需要先进入队列中等待。 
    本文以非公平锁NonfairSync的sync实例进行分析。

      public ReentrantLock() {
            sync = new NonfairSync();
        }
    
        public ReentrantLock(boolean fair) {
            sync = (fair)? new FairSync() : new NonfairSync();
        }

    由图1可知,NonfairSync继承自Sync,因此也继承了AbstractQueuedSynchronizer中的所有方法实现。接着进入NonfairSync的lock()方法。

     final void lock() {
                // 利用cas置状态位,如果成功,则表示占有锁成功
                if (compareAndSetState(0, 1))
                    // 记录当前线程为锁拥有者
                    setExclusiveOwnerThread(Thread.currentThread());
                else
                    acquire(1);
            }

    在lock方法中,利用cas实现ReentrantLock的状态置位(cas即compare and swap,它是CPU的指令,因此赋值操作都是原子性的)。如果成功,则表示占有锁成功,并记录当前线程为锁拥有者。当占有锁失败,则调用acquire(1)方法继续处理。

       public final void acquire(int arg) {
            //尝试获得锁,如果失败,则加入到队列中进行等待
            if (!tryAcquire(arg) &&
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
                selfInterrupt();
        }

    acquire()是AbstractQueuedSynchronizer的方法。它首先会调用tryAcquire()去尝试获得锁,如果获得锁失败,则将当前线程加入到CLH队列中进行等待。tryAcquire()方法在NonfairSync中有实现,但最终调用的还是Sync中的nonfairTryAcquire()方法。

      protected final boolean tryAcquire(int acquires) {
                return nonfairTryAcquire(acquires);
            }
    
            final boolean nonfairTryAcquire(int acquires) {
                final Thread current = Thread.currentThread();
                // 获得状态
                int c = getState();
                // 如果状态为0,则表示该锁未被其他线程占有
                if (c == 0) {
                    // 此时要再次利用cas去尝试占有锁
                    if (compareAndSetState(0, acquires)) {
                        // 标记当前线程为锁拥有者
                        setExclusiveOwnerThread(current);
                        return true;
                    }
                }
                // 如果当前线程已经占有了,则state + 1,记录占有次数
                else if (current == getExclusiveOwnerThread()) {
                    int nextc = c + acquires;
                    if (nextc < 0) // overflow
                        throw new Error("Maximum lock count exceeded");
                    // 此时无需利用cas去赋值,因为该锁肯定被当前线程占有
                    setState(nextc);
                    return true;
                }
                return false;
            }

    在nonfairTryAcquire()中,首先会去获得锁的状态,如果为0,则表示锁未被其他线程占有,此时会利用cas去尝试将锁的状态置位,并标记当前线程为锁拥有者;如果锁的状态大于0,则会判断锁是否被当前线程占有,如果是,则state + 1,这也是为什么lock()的次数要和unlock()次数对等;如果占有锁失败,则返回false。 
    在nonfairTryAcquire()返回false的情况下,会继续调用acquireQueued(addWaiter(Node.EXCLUSIVE), arg))方法,将当前线程加入到队列中继续尝试获得锁。

        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;
        }
    
        private Node enq(final Node node) {
            // CAS方法有可能失败,因此要循环调用,直到当前线程的节点加入到队列中
            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;
                    }
                }
            }
        }

    addWaiter()是AbstactQueuedSynchronizer的方法,会以节点的形式来标记当前线程,并加入到尾节点中。enq()方法是在节点加入到尾节点失败的情况下,通过for(;;)循环反复调用cas方法,直到节点加入成功。由于enq()方法是非线程安全的,所以在增加节点的时候,需要使用cas设置head节点和tail节点。此时添加成功的结点状态为Node.EXCLUSIVE。 
    在节点加入到队列成功之后,会接着调用acquireQueued()方法去尝试获得锁。

        final boolean acquireQueued(final Node node, int arg) {
            try {
                boolean interrupted = false;
                for (;;) {
                    // 获得前一个节点
                    final Node p = node.predecessor();
                    // 如果前一个节点是头结点,那么直接去尝试获得锁
                    // 因为其他线程有可能随时会释放锁,没必要Park等待
                    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;
            }
        }

    在acquireQueued()方法中,会利用for (;;)一直去获得锁,如果前一个节点为head节点,则表示可以直接尝试去获得锁了,因为占用锁的线程随时都有可能去释放锁并且该线程是被unpark唤醒的CLH队列中的第一个节点,获得锁成功后返回。 
    如果该线程的节点在CLH队列中比较靠后或者获得锁失败,即其他线程依然占用着锁,则会接着调用shouldParkAfterFailedAcquire()方法来阻塞当前线程,以让出CPU资源。在阻塞线程之前,会执行一些额外的操作以提高CLH队列的性能。由于队列中前面的节点有可能在等待过程中被取消掉了,因此当前线程的节点需要提前,并将前一个节点置状态位为SIGNAL,表示可以阻塞当前节点。因此该函数在判断到前一个节点为SIGNAL时,直接返回true即可。此处虽然存在对CLH队列的同步操作,但由于局部变量节点肯定是不一样的,所以对CLH队列操作是线程安全的。由于在compareAndSetWaitStatus(pred, ws, Node.SIGNAL)执行之前可能发生pred节点抢占锁成功或pred节点被取消掉,因此此处需要返回false以允许该节点可以抢占锁。 
    当shouldParkAfterFailedAcquire()返回true时,会进入parkAndCheckInterrupt()方法。parkAndCheckInterrupt()方法最终调用safe.park()阻塞该线程,以免该线程在等待过程中无线循环消耗cpu资源。至此,当前线程便被park了。那么线程何时被unpark,这将在unlock()方法中进行。 
    这里有一个小细节需要注意,在线程被唤醒之后,会调用Thread.interrupted()将线程中断状态置位为false,然后记录下中断状态并返回上层函数去抛出异常。我想这样设计的目的是为了可以让该线程可以完成抢占锁的操作,从而可以使当前节点称为CLH的虚拟头节点。

        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) {
                // 如果前面的节点是CANCELLED状态,则一直提前
                do {
                    node.prev = pred = pred.prev;
                } while (pred.waitStatus > 0);
                pred.next = node;
            } else {
                compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
            } 
            return false;
        }
    
        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);
        }

    2、unlock()方法的实现 
    同lock()方法,unlock()方法依然调用的是sync.release(1)。

        public final boolean release(int arg) {
            // 释放锁
            if (tryRelease(arg)) {
                Node h = head;
                // 此处有个疑问,为什么需要判断h.waitStatus != 0
                if (h != null && h.waitStatus != 0)
                    unparkSuccessor(h);
                return true;
            }
            return false;
        }
    
        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()方法实现了锁的释放,逻辑上即是将锁的状态置为0。当释放锁成功之后,通常情况下不需要唤醒队列中线程,因此队列中总是有一个线程处于活跃状态。

    总结: 
    ReentrantLock的锁资源以state状态描述,利用CAS则实现对锁资源的抢占,并通过一个CLH队列阻塞所有竞争线程,在后续则逐个唤醒等待中的竞争线程。ReentrantLock继承AQS完全从代码层面实现了java的同步机制,相对于synchronized,更容易实现对各类锁的扩展。同时,AbstractQueuedSynchronizer中的Condition配合ReentrantLock使用,实现了wait/notify的功能。

    参考::https://blog.csdn.net/yanyan19880509/article/details/52345422  

         https://blog.csdn.net/jiangjiajian2008/article/details/52226189   

    http://www.cnblogs.com/skywang12345/p/3496101.html

  • 相关阅读:
    To My Girlfriend (DP)
    A Simple Nim (SG打表找规律)
    A Boring Question (打表)
    A Simple Chess (Lucas组合数 + 容斥)
    World is Exploding (容斥 + 统计)
    Two (DP)
    K-wolf Number (数位DP)
    Interesting (manacher + 前缀和处理)
    How Many Triangles (极角排序 + 尺取法)
    Divide the Sequence (贪心)
  • 原文地址:https://www.cnblogs.com/xiaolovewei/p/9113829.html
Copyright © 2020-2023  润新知