ReentrantLock的公平与非公平核心区别

ReentrantLock的公平与非公平核心区别,第1张

ReentrantLock的公平与非公平核心区别

先看类图,NonfairSync和FairSync共同构成了ReentrantLock,他们是ReentrantLock的内部类

ReentrantLock只是对外的一个壳,具体实现都是由这两个内部类完成的

那么这两个内部类的共同特点现在看来都是继承自Sync这个类

1.核心区别

直接上源码

static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }

static final class FairSync extends Sync {
        private static final long serialVersionUID = -3000897897090466540L;

        final void lock() {
            acquire(1);
        }
//唯一区别就是这里,其余的入队,阻塞,唤醒,都使用Sync类相同的代码
//公平锁上来不会直接进行一次抢锁,而非公平锁会抢锁,抢到锁就不入队了,直接执行,那么对于队列里的而言就不公平了,但是如果每个线程都抢不到,还是要入队,比如连续来100个,这100个都没有抢到锁过的,那么其实他们还是个公平的,所以这个公平是相对宏观还是微观
2.相同的 *** 作

加锁

public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }
这个方法为获取锁,分为以下几步:
1. tryAcquire,能否获取到锁,由AQS的子类进行实现,AQS只做抽象不做具体实现细则
    以ReentrantLock的公平锁为例
   
       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;
                }
            }
            //如果state为0,表示可以尝试获取锁,因为公平锁,还要判断队列里面有没有排在前面的
            //队列没有并且state为0   就使用cas为自己这个线程加锁执行代码
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            //这里还要判断一下是不是自己线程重复加锁   重复加锁也可以加
            return false;
        }
2. 如果没有获取到锁   

    tryAcquire = false     !tryAcquire(arg) = true 不会短路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)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
     }
     // 下图      

3. 入队成功以后

    如果是头节点,不会进行阻塞,尝试再次获取锁,减少资源消耗
    执行 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;
            }
    //这里如果获取到锁会将head指针后移,并且通过setHead方法擦除head结点指向的结点的信息,让head还是指向没有信息的空结点
    //通常我们一般可能会使用将head指向的下一个结点移除,head的位置不变,但是这里使用的是擦除信息的方式
            
4. 如果入队成功
    并且没进行获取锁成功以后就要进行阻塞
        阻塞的过程是两轮循环
        1:通过shouldParkAfterFailedAcquire方法将head指向的空结点的waitStatus更改成SIGNAL表示后面的结点可以被唤醒
        2:进行真实的阻塞标志位设置 *** 作,返回true,这里也仅仅是设置了要阻塞的标志位

        private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            return true;
        if (ws > 0) {
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }
    //由上面可知,head一直指向的是一个空结点,第二个结点才是我们线程结点
5. 入队成功,得到允许阻塞标志位以后,才是进行真正的阻塞 *** 作
    if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

解锁

public void unlock() {
        sync.release(1);
    }
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;
    }
​
    private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        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);
    }
​
    waitestate = 0 - > -1 head节点为什么改到-1,因为持有锁的线程T0在释放锁的时候,得判断head节点的waitestate是否!=0,如果!=0成立,会再把waitstate = -1->0,要想唤醒排队的第一个线程T1,T1被唤醒再接着走循环,去抢锁,可能会再失败(在非公平锁场景下),此时可能有线程T3持有了锁!T1可能再次被阻塞,head的节点状态需要再一次经历两轮循环:waitState = 0 -> -1
   Park阻塞线程唤醒有两种方式:
    1、中断
    2、release()
因为队列的头是一个空结点,而不是第一个要获得锁的结点,所以要控制head的waitestate来标记后面想要获取锁的结点是否能被解开阻塞的状态
​
不管是公平锁还是非公平锁,唤醒的都是队列最前面的一个,而不是都唤醒一起再竞争一次,非公平也要入队,只要入队,那么他们之间就不在竞争 
之前对公平和非公平有一个误区,以为非公平锁要在解锁时要唤醒所有的队列中线程再竞争一次,其实不然,底层的LockSupport并没有唤醒所有的能力,即便可以,都唤醒竞争非常大,资源消耗高,所以只要入队就每次只唤醒一个,因为每次一个必然只要入队就有序了
3.公平锁和非公平锁效率问题

我们设想这样一个场景,这里我们使用Thread1   Thread2这样表示一个线程
在一段时间内一共有1000个线程访问,其中每个线程执行的时间还不是特别的短,平均一个线程运行的时间内会有10个线程到达

公平锁场景:
Thread1获取到锁开始执行,这时Thread2-Thread11到达,Thread2-Thread12入队,队列空时,队头加入的第一个Thread2会尝试抢一次锁,没抢到阻塞,那么这时Thread2-Thread12都阻塞了,此时Thread1执行完释放锁,Thread2被唤醒执行,Thread2执行的过程中,Thread12-Thread22入队,并且这些在队列中都不是队头,并且都不是在队空时加入的第一个线程,只有满足这两个条件才会尝试获取锁,获取不到再阻塞,那么由此可以推断,Thread2-Thread1000中,只有Thread尝试获取锁还没获取到阻塞了,其余的都是直接阻塞

非公平锁:
Thread1获取到锁开始执行,这时Thread2-Thread11到达,Thread2-Thread12入队,队列空时,队头加入的第一个Thread2会尝试抢一次锁,没抢到阻塞,那么这时Thread2-Thread12都阻塞了,此时Thread1执行完释放锁,这时想要去唤醒Thread2让它执行,但是Thread13来了直接抢到锁执行,由此一直到1000,可能会产生100个线程没有入队阻塞,直接来了就抢锁执行

我们知道阻塞唤醒是比较消耗资源的,涉及到cpu的状态切换,那么同样是1000次,非公平锁有100次不需要阻塞唤醒,而公平锁一次没有,性能的差距就来了,但是带来的问题也是非常大的,如果每个线程运行时间十分精准,Thread2要等到前面100个上来就抢锁的强盗都执行完才执行,这就比较不友好了
当然我举例的时间可能不恰当,执行时间和到达线程太紧凑了,这肯定是不合适的,有可能一直拿不到锁饿死很多线程,这里只是拿时间开销做一个类比,具体使用公平非公平要根据线程的运行时间,并发量,队列的堆积数量以及需不需要强制公平来处理业务决定

下图为一个AQS的大体流程,对照着可以看到公平非公平源码中哪里是公用的,哪里是私有的

欢迎分享,转载请注明来源:内存溢出

原文地址: https://outofmemory.cn/zaji/5715887.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-12-17
下一篇 2022-12-17

发表评论

登录后才能评论

评论列表(0条)

保存