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

阅读: 评论:0

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

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());elseacquire(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 failureNode pred = tail;if (pred != null) {node.prev = pred;if (compareAndSetTail(pred, node)) { = 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 GCfailed = 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); = 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() {lease(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 = ;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 -> -1Park阻塞线程唤醒有两种方式: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的大体流程,对照着可以看到公平非公平源码中哪里是公用的,哪里是私有的

本文发布于:2024-02-02 09:24:30,感谢您对本站的认可!

本文链接:https://www.4u4v.net/it/170683707142861.html

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。

标签:公平   与非   区别   核心   ReentrantLock
留言与评论(共有 0 条评论)
   
验证码:

Copyright ©2019-2022 Comsenz Inc.Powered by ©

网站地图1 网站地图2 网站地图3 网站地图4 网站地图5 网站地图6 网站地图7 网站地图8 网站地图9 网站地图10 网站地图11 网站地图12 网站地图13 网站地图14 网站地图15 网站地图16 网站地图17 网站地图18 网站地图19 网站地图20 网站地图21 网站地图22/a> 网站地图23