ReentrantLock详解

1 篇文章 0 订阅
1 篇文章 0 订阅
ReentrantLock详解


1.核心部分
AbstractQueuedSynchronizer 核心操作类(不仅仅是在Lock领域,线程池也多有应用,俗称AQS)
Sync                            继承AQS,提供了对Lock的部分操作
NonfairSync 继承Sync,见名知意,非公平锁的操作(默认为非公平所)
FairSync                        继承Sync,见名知意,公平锁的操作




2.获取锁的过程
这里默认使用非公平锁(公平锁逻辑上类似。细节有所不同)


===========================================
方式1:lock();
方式2:tryLock();
方式3:tryLock(long timeout, TimeUnit unit);
===========================================


方式1:lock();


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


    个人认为比较好理解,首先进行一个原子操作compareAndSetState(0, 1),lock的state在初始状态或者未被占有的状态下为0,这里是一种基于乐观锁的设计。
    若是成功设置lock的state,设置当前线程为lock的owner。
    否者进入acquire(1);


    acquire(1);


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


    官方释义:以独占模式来获取锁,且忽略中断。若成功获取锁,返回。否者有可能进入队列等待。


    if判断条件分为2个,分别解析。
    (1)tryAcquire(arg)


    定义于AQS内,实现在ReentrantLock的NonfairLock中
    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;
   }


首先获取当前线程,判断当前lock的state(万一在我们第一次获取锁失败的时候刚好有人释放了呢),若lock的state是0,则尝试再去取compareAndSetState(0, acquires),成功返回true,否则返回false。
若是state!=0,两种情况。1.该线程本身就是lock的持有者,由于ReentrantLock支持重入,所以在符合最大重入次数的条件下,设置state++,返回true。
  2.该线程不是lock的持有者,那么相当于又一次尝试获取锁失败。返回false.


tips:我认为这里能体现部分作为“非公平所”的特性,就是说,一个线程在第一次获取锁失败的情况下,还有第二次获取的机会,假设他与一个在等待队列中等待了许久的线程竞争,结果他回去了锁,是不是就意味着不公平?   


再看acquire(int arg)的if语句中的2个条件,如果第一个tryAcquire返回的是true,根据短路与的特性,后面的都不执行,线程成功的获取lock,返回。
若是返回的是false,我们继续看接下来的条件
(2)acquireQueued(addWaiter(Node.EXCLUSIVE), arg)


先看addWaiter(Node.EXCLUSIVE)


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)) {//设置新的tail结点
                pred.next = node; //由于是双向链表,所以要设置prev以及next
                return node;
            }
        }
        enq(node);
        return node;
    }


    首先,根据函数名就能猜到,该操作的结果是产生一个等待着,也就是使当前线程加入等待队列。
    这里介绍下内部等待队列的数据结构。
    注意:是一个双向链表,这里并没有标出。
     * +------+  prev +-----+       +-----+
     * head |      | <---- |     | <---- |     |  tail
     *      +------+       +-----+       +-----+
    Node node = new Node(Thread.currentThread(), mode);可以看到根据传入的Node的mode来创建出一个node,该node的内部thread指向当前线程。
    tail指向尾节点,head指向头结点,可以认为,在第一个线程进入等待队列时,tail以及head都是空的。
    所以先来关注enq(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;
                }
            }
        }
    }


    我们来看,第一个将要等待队列的线程到来时,t必然为null。所以第一次会触发compareAndSetHead(new Node()),结果也就是产生一个辅助节点作为head节点。
    然后我们需要关注的是这是一个for(;;),也就是死循环,第二次循环时,由于head节点已经被创建,那么node就会被插入到链表尾部,也就是prev指向前一个节点,tail指向该node。
    说白了就是若等待队列为空,构造出一个等待队列(辅助的head结点,tail也指向head),再将传入的node插入队列。移动tail指针。


    回过头去看addWaiter(Node.EXCLUSIVE)
    其实做的只是插入一个结点,并且移动tail指向新插入的结点。


    再来看acquireQueued(addWaiter(Node.EXCLUSIVE), arg)


    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 (p == head && tryAcquire(arg)),由于p是传入结点的前一个结点,如果p是head,则再次尝试去获取锁,如果获取到了,开始setHead(node)操作。
    private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }
    setHead(node);做的只是将head指向该node,同时置空一些node的信息,因为他已经获取到锁了,所以不需要再等待队列中了。
    由此可见,在等待队列中最有可能能回去lock的是head后的第一个结点。
    若是if (p == head && tryAcquire(arg))不满足,则进入 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) 分支。
    shouldParkAfterFailedAcquire(p, node)


    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            return true;
        if (ws > 0) { //大于0 表示已经被cancel掉了 也就是无效的了          
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {         
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }


    这段代码不是很直面。
    首先waitStatus的含义是指该结点内thread的等待状态,由以下几种组成:
    static final Node EXCLUSIVE = null;


        static final int CANCELLED =  1; //表示该线程已取消
        
        static final int SIGNAL    = -1; //标示该线程需要被挂起
       
        static final int CONDITION = -2; //标示该线程正在等待信号量
       
        static final int PROPAGATE = -3; //等待共享锁


    if (ws > 0) 若是满足此条件,从该结点开始,向前扫描,找到第一个状态不为cancel的结点。然后与找到的结点连接(next,prev),若是下一个结点的状态小于0,则尝试将其置为SIGNAL。
    在关注外部的for(;;)是一个死循环,也就是说,第二次来遍历的时候必然会发现状态为SIGNAL的结点(不为cancel)。找到后返回true。


    parkAndCheckInterrupt()
    该操作只是将当前线程挂起到WAITING状态,等待一个中断或者unpark()方法来唤醒他。
    若是if操作成功,则将Interrupt的值设置为true,表示需要中断来唤醒。


    再回到顶层


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


    假设此时tryAcquire(arg);没有获取到锁,return false; acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 返回false 说明等待队列的head之后的第一个结点已获取锁,且Interrupt的值为false,代表不需要中断操作来唤醒。若返回true,则说明队列中有一个结点已经获取锁,但是需要唤醒当前线程。
    所以selfInterrupt();该操作的目的就是产生一个中断,是线程从WAITING中恢复。


    具象一点的说法,假设现有N个线程未获取lock进入到了等待队列,每个线程都在进行这样的无限循环来判断自己是否是head之后的第一个有效的结点,如果是,则尝试去获取lock,不是则继续循环(CAS的体现)。
    比如队列中第3个结点与第5个结点,两者都在进行循环,尝试将两者间的无效(cancel)的结点去掉,假设第4个结点的state为cancel,那么第3个结点自然就会与第5个结点连接。再假设第3个结点的前面两个结点都是有效结点,那么在判断p==head的时候返回false,所以继续循环,其实这是的循环我感觉已经是某种意义上的无用循环了,因为第二个结点状态始终有效。但是一旦找到在该节点前的第一个有效结点后,会使当前线程进入WAITTING的状态,等待unpark()或interrupt()使线程恢复,也就是说,哪怕此时结点是head后的第一个结点,也拿到了lock,但是线程却处于WAITTING状态,所以需要selfInterrupt();来触发一个中断,来恢复线程状态!


    概括的流程:
    第一次获取(失败)---> 第二次获取(失败)---> 插入队列---> 进入死循环 ---> 有该结点向前找,找第一个有效结点 ---> 找到后,当前线程挂起,进入WAITTING ---> 判断当前结点的前一个是否为head,若是则尝试获取lock ---> 获取到锁,interrupt(),是当前线程从中断中恢复。


    lock()的总结:
    1.线程未获取到锁进入队列后的操作(死循环在做什么)
    2.线程从开始获取锁到进入阻塞队列的过程(3次获取锁的操作,体现非公平性)
    3.多次的CampareAndSet操作,达到的目的
    3.乐观锁的设计方式,体现在哪里




    关于tryLock()
    内部实际是调用了nonfairTryAcquire(1);所以实现原理上几乎一样


关于tryLock(long timeout, TimeUnit unit);
从函数名上就可以知道,这是自带超时机制的获取方式,在超出既定响应时间后没有获取到lock,抛出InterruptException,内部实现原理类似,但是多了计时机制。








3.释放锁的过程


1.unlock();
2.tryRelease();


unlock();


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


    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其实做的是设置锁的state,比较容易看出,判断当前线程是否为锁的拥有者,若是,锁的计数器-1,若是计数器为0,则清空锁的拥有者,因为ReentrantLock是排他锁,但是也是重入锁,所以允许一个线程多次获取锁,基于这个情况,只有lock的次数与unlock的次数对应,才能释放掉锁。


    release做的就是释放锁之后唤醒head之后的第一个有效结点的线程。


    因为在设计的时候任务head之后的第一个有效结点是有资格去竞争锁的,那么head一般就认为是已经完成任务的线程所在的结点(当然,不这样认为,仅仅认为是一个head指针也未尝不可)。


    总结下unlock()
    锁计数器-1 ---> 若计数器为0 ---> 释放锁,否则不释放 ---> 将head之后的第一个有效结点唤醒


    重点:
    1.ReentrantLock存在重入,所以需要判断锁计数器是否为0.
    2.释放锁其实做的是lock的state置0,owner置null。
    3.释放锁之后,将head之后第一个有效结点唤醒。


    以上就是对于ReentrantLock的个人理解



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值