ReentrantLock的公平锁的竞争

ReentrantLock的lock方法
    public void lock() {
        sync.lock();
    }
View Code
ReentrantLock.FairSync的lock方法
final void lock() {
       acquire(1);
 }
View Code

AQS的acquire方法

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
View Code
1、tryAcquire方法
protected final boolean tryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            int c = getState();
            if (c == 0) {
                if (!hasQueuedPredecessors() &&
                    compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            return false;
        }
    }
View Code
  • 首先获取当线程
  • 获取锁的当前状态state值,0代表无线程占用,不为0(大于0)时锁已经被占用了
  • 如果锁无线程占用state=0,且当前线程不需要排队(hasQueuedPredecessors方法返回false),下一步就是执行CAS来获得锁(此时锁state的状态要由0变为了1),CAS保证了同时争夺锁的并发安全性,如果此时CAS失败,就要重头来(本质上是CAS修改state从0变为1)。如果抢到了锁,将锁的拥有者改成当前线程,tryAcquire返回true,当前线程竞争到锁。 这里要注意,第一次插入的null问题
  • 如果锁已经被占用state!=0,分为两种情况:
    1. 其他线程占有这把锁,tryAcquire直接返回 false(当前线程加锁失败);
    2. 当前线程占有这把锁,再次加锁state值加1,tryAcquire返回true,相当于加了多层锁,代表这个锁进入多次需要被释放多次,才可以被其他线程获取。这种情况不需要使用CAS保证线程安全性的,因为上一层已经有锁在保护这个线程的安全性。(注意这种情况可能抛异常)
   公平锁竞争是否需要排队:
AQS(AQS的实现原理)中的hasQueuedPredecessors方法是用来判断线程是否需要排队的(返回false不需要排队)。因为是公平锁,所以必须要判断是,否则就不公平了。对于非公平锁没有判断排队操作,只要一个线程释放了锁后来的线程也是可以抢到的(非公平锁的实现就是少了这个判断)
public final boolean hasQueuedPredecessors() {
        // The correctness of this depends on head being initialized
        // before tail and on head.next being accurate if the current
        // thread is first in queue.
        Node t = tail; // Read fields in reverse initialization order
        Node h = head;
        Node s;
        return h != t &&
            ((s = h.next) == null || s.thread != Thread.currentThread());
    }
View Code
  • 头结点是否等于尾结点

  • 头结点的下一个节点是否尾空

  • 或者下一个结点的线程是否为当前线程

1、首先,判断头结点是否等于尾结点。只有在初始化的时候头结点才会等于尾结点,或者队列为空,头尾结点都为null。所以头结点等于尾结点(直接返回false),代表队列为空不需要排队。

2、第一个判断已经知道头尾结点之间存在其他结点,但由于前面的enq方法是先处理头结点的,然后除了两个CAS操作之外,其他都不是原子性的,也就是基本上整个enq方法都不是原子性的,所以当正在插入第一个线程的时候,会出现一个h.next == null的问题(代表有一个线程正在插入还没完成,所以当前线程需要排队),所以当h.next == null时就代表要排队

3、经过前面两次判断,可以得知队列中有人在排队,并且此时并不存在队列第一次插入结点的Null问题。最后一个判断就是判断头结点的下一个线程是不是当前线程(下一个轮到你,不需要排队;因为插入方法是尾插法,所以弹出方法要从头弹出)

       setExclusiveOwnerThread:当判断前面无人排队,或者下一个就是轮到当前线程,CAS操作返回true,那么首先要做的是调用setExclusiveOwnerThread方法,将                           exclusiveOwnerThread改成为自己,表明这个锁被这个线程获取了。

    protected final void setExclusiveOwnerThread(Thread thread) {
        exclusiveOwnerThread = thread;
    }
View Code
2、addWaiter方法当前线程竞争锁失败后,tryAcquire()方法返回false后,调用AQS的addWaiter()方法以当前线程创建等待节点加入队列。addWaiter()底层队列里面的结点是怎么添加的?
addWaiter()方法完成一个Node结点加入底层等待队列(也就是入队操作),返回当前插入的结点,相当于是尾结点。
不过这里要注意的是参数Node.EXCLUSIVE,本质上这只是一个状态表示,表示入队这个结点是进入等待状态。
 private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }
  • 首先以当前线程创建一个Node等待节点实例
  • 判断ReentrantLock的实例tali节点是否为null?
    1. tail!=null代表不是第一次插入,直接用尾插法将当前线程创建的等待节点加入队列;
    2. tail==null代表第一次插入,说明是第一次向队列中加入等待节点,这时需要调用AQS的enq()方法,先初始化队列的head和tail节点,enq方法是针对第一次插入使用的;

enq()方法里面是一个死循环,第一次插入先创建一个新结点为头结点(相当于一个哨兵)同时tail=head新尾结点一样,完成锁队列head和tail节点的初始化;接下来下一轮循环,此时尾结点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方法呢?如果并不是第一次入队,因为前面的addWaiter,使用了一次CAS,那如果CAS失败了怎么办?即中途有线程把尾结点改了怎么办?此时就需要重复的操作,不断的执行CAS直到成功为止,所以就有了无论哪次插入都要执行enq方法,因为enq方法里面就是一个死循环的CAS执行,直到CAS执行成功,才会结束。总结一下添加结点,所以头结点只是一个哨兵,是不存储线程的,而插入的方法则是尾插法。其实头结点也不完全是哨兵,下面我们看线程自旋时,会发现其实头结点是当前轮到执行的线程。

3、acquireQueued方法

前面已经判断了是否需要等待,如果需要等待,就会先执行插入队列的方法,然后执行acquireQueued。既然进入队列了,那么此时就应该是park此时的线程,也就是停止此时的线程,但其实停止并不是休眠或者阻塞,在ReentrantLock中,是使用自旋的方式来进行的。

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;                                                        //定义一个failed变量,该变量用来判断当前线程拿到锁是否失败
        try {                                                                                                        
            boolean interrupted = false;                                              //定义一个interrupted变量,这个变量用来判断当前线程是否还在排队(还在自旋)
            for (;;) {                                                                //死循环,这个死循环就是底层的CAS自旋
                final Node p = node.predecessor();                                    //获取当前线程结点的前一个结点
                
                //如果前一个线程是头结点,代表当前线程要时刻注意前一个线程是否结束,然后当前线程就会循环看是否需要排队。前面已经提到过,头结点的线程要么是一个哨兵,要么就是当前拥有锁的线程。
                // 如果前一个结点并不是头结点,因为是公平锁,那就不需要在意,继续等待
                if (p == head && tryAcquire(arg)) {
                    //进入到这里,就代表前一个线程已经结束了,并且当前线程抢到锁,正在执行
                    setHead(node);                                                    // 先将自己改成头结点,代表自己正在执行,让下一个线程注意。setHead不仅仅是让自己成为头结点,同时将自己里面的线程设为null,因为要满足线程队列里不存在拥有锁的线程原则。
                    p.next = null;                                                    // help GC,断开原来头结点与自己的连接
                    failed = false;                                                   // failed变量设为false,代表线程拿到锁
                    return interrupted;                                               //返回interrupted,该线程结束自旋,开始执行
                }
                 //当争夺锁失败时,调用shouldParkAfterFailedAcquire方法,检查和更新未能获取锁的线程状态,判断是否需要进行阻塞
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())                                          // 如果需要阻塞就会调用parkAndCheckInterrupt,也就是让这个线程进行阻塞
                    interrupted = true;                                               //获取不到锁,failed依然为true,获取不到锁,还需要继续自旋,执行下一轮循环
            }
        } finally {
            //当轮到线程执行时,需要结束自旋,开始执行动作,即failed会变为false
            if (failed)
                cancelAcquire(node);
        }
    }
自旋循环简单,但是一直自旋并不是好事,因为自旋会消耗CPU,假如有一个线程迟迟不释放锁,就白白消耗了很多CPU。所以,自旋要限制次数,而限制次数就存在于当获得锁失败时可能会调用的两个方法
    private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }

setHead方法将头结点里的线程设为null,因为要满足获取锁的线程不在队列中,代表获取锁的线程已经不需要再进行排队了,而且不需要被叫醒。

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;   //获取前一个线程的waitStatus状态
        if (ws == Node.SIGNAL)      //如果前一个线程状态为SIGNAL
            return true;            //返回true,即当前线程需要阻塞、休眠
        if (ws > 0) {               //如果前一个线程的状态大于0,代表前一个线程取消执行了,就要从队列中删除前一个线程
            do {  
                node.prev = pred = pred.prev;  //让当前线程的前一个线程为前前一个线程
            } while (pred.waitStatus > 0);   //当然这是一个循环,可能前面有其他取消的线程,那么也是要删除的
            pred.next = node;
        } else {  //如果是0或者其他状态
             //那么前一个线程还是可以继续自旋的,不过将状态改成了SIGNAL
             //当前线程下一轮抢锁如果还是失败,那他的上一个线程就会因为变成了SIGNAL
             //而被取消线程,转而变成阻塞了
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        //不需要park掉线程
        return false;
    }
shouldParkAfterFailedAcquire方法前面已经提到过这个线程是用来判断获取锁失败的线程是否需要park,即是否需要阻塞(因为CAS太多次了)

SIGNAL状态是什么?

这个值为-1,线程的waitStatus为-1代表这个线程不可以自旋了,要阻塞、休眠了,所以waitStatus为-1可以表示线程休眠。这里会有一个问题,为什么要队列中的上一个线程决定当前线程是否需要睡眠(为什么要修改上一个线程的waitStatus为SIGNAL),即挂起当前线程?这是因为前面的都等的休眠了,后面的更加需要进行休眠(前面的都等的睡着了,后面的也要睡着,毕竟公平锁要前面的执行完才会到后面),但其实这个waitStatus只是一个标志性睡眠,不代表线程真的被挂起了。那么又有一个问题,自旋几次才会进入休眠状态?答案是自旋两次,第一次自旋,抢不到锁,在shouldParkAfterFailedAcquire里将上一个线程视为睡眠,也就是将其waitStatus状态改为SIGNAL,第二次自旋,发生前面的线程已经是睡眠了,那么就执行parkAndCheckInterupt方法方法挂起。

parkAndCheckInterupt()方法就是让线程挂起,只有判断了线程需要挂起才会去执行,这个方法的作用是,让线程进入等待状态。等待状态可以被两种动作唤醒:

(1)其他线程尝试对该线程进行interrupt(2)被其他线程进行unpark。如果是被其他线程进行interrupt唤醒的,就代表有其他线程想终止当前线程,那么Thread.interrupted就会返回true,如果不是,是第二种方式的,就会返回false

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

可以直到其调用了LockSupport.part方法,这个方法其实就是禁用线程的,也就是将线程挂起。LockSupport的设计实现

可以看到acquireQueued方法里面,进行了一系列try语句里面的流程之后,最后还有一个finally语句块,这个语句块是调用了cancelAcquire的,而且要在failed变量为true的时候才会执行。现在问题来了,failed变量在自旋死循环中,只会变成false,而不会变成true,而且线程挂起之后更加不会走到这行代码,那么什么时候会调用这个acquireQueued方法呢?这里要执行acquireQueued方法的唯一途径就是try语句块里面抛出了异常,中断了try里面的代码,而且try里面的代码并没有将failed修改为true

那么这个抛出异常的地方在那里呢?回顾一路走过来,抛出异常处只有一个地方,tryAcquire()方法里

也就是当调用tryAcquire去看是否需要排队时,当拥有锁的线程又再一起去申请获得锁,可能会抛出异常,不过这个异常抛出也是有前提的,也就是锁的状态要小于0?这个cancelAcquire方法,实际上就是让这个线程取消获取锁。

private void cancelAcquire(Node node) {
        // Ignore if node doesn't exist
        if (node == null)
            return;

        node.thread = null;

        // Skip cancelled predecessors
        Node pred = node.prev;
        while (pred.waitStatus > 0)
            node.prev = pred = pred.prev;

        // predNext is the apparent node to unsplice. CASes below will
        // fail if not, in which case, we lost race vs another cancel
        // or signal, so no further action is necessary.
        Node predNext = pred.next;

        // Can use unconditional write instead of CAS here.
        // After this atomic step, other Nodes can skip past us.
        // Before, we are free of interference from other threads.
        node.waitStatus = Node.CANCELLED;

        // If we are the tail, remove ourselves.
        if (node == tail && compareAndSetTail(node, pred)) {
            compareAndSetNext(pred, predNext, null);
        } else {
            // If successor needs signal, try to set pred's next-link
            // so it will get one. Otherwise wake it up to propagate.
            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
        }
    }
View Code

 

参考链接:

https://blog.csdn.net/AK774S/article/details/124664227

https://blog.csdn.net/GDUT_Trim/article/details/117968929

posted @ 2022-11-25 16:50  雨也飘柔  阅读(48)  评论(0)    收藏  举报