先看类图,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); }
3.公平锁和非公平锁效率问题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并没有唤醒所有的能力,即便可以,都唤醒竞争非常大,资源消耗高,所以只要入队就每次只唤醒一个,因为每次一个必然只要入队就有序了
我们设想这样一个场景,这里我们使用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的大体流程,对照着可以看到公平非公平源码中哪里是公用的,哪里是私有的
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)