以 ReentrantLock 为例,主要讲解ReentrantLock 中公平锁的实现步骤(FairSync):
ReentrantLock 继承Lock类,并提供了加锁(Lock)和解锁(UnLock)两个方法,但是方法内是调用了Snyc类的方法实现,而Snyc类其实是继承了AbstractQueuedSynchronizer这个类 也就是AQS实现的,所以最终 ReentrantLock 底层还是由AQS去实现的。
//实现Lock接口 public class ReentrantLock implements Lock, java.io.Serializable { private static final long serialVersionUID = 7373984872572414699L; /** 提供所有实施机制的同步器 内部类*/ private final Sync sync; /** 抽象类Sync继承了AQS */ abstract static class Sync extends AbstractQueuedSynchronizer { private static final long serialVersionUID = -5179523762034025860L; /** * Performs {@link Lock#lock}. The main reason for subclassing * is to allow fast path for nonfair version. */ abstract void lock();
//ReentrantLock类中加锁的方法 是调用sync实现 而Sync抽象类继承AQS 最终逻辑还是有AQS完成 public void lock() { sync.lock(); }
首先得先看看AQS内部阻塞队列的数据结构:请看下图:
1.ReentrantLock 在内部用了内部类 Sync 来管理锁,所以真正的获取锁和释放锁是由 Sync 的实现类来控制的。
abstract static class Sync extends AbstractQueuedSynchronizer { }
2.Sync 有两个实现,分别为 NonfairSync(非公平锁)和 FairSync(公平锁),我们看 FairSync 部分。
注意:初始化ReentrantLock对象 不传参数 默认是非公平锁
public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
3.由乐观锁进来 最主要,最核心的几个方法 这块理解了 也就差不多理解他的逻辑了
/** 以独占模式获取,忽略中断。通过至少调用一次tryAcquire来实现,成功后返回。否则,线程将排队,可能会重复阻塞和 取消阻塞,调用tryAcquire直到成功。此方法可用于实现方法Lock.Lock。 */ public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
//第一步 tryAcquire方法 先尝试获取锁 protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); //1.获取当前锁状态是否是无锁空闲状态 if (c == 0) { //2.是无锁状态 则再判断队列中是否有排队的线程 判断自己是不是排在头节点的下一个或者当前线程不是自己 if (!hasQueuedPredecessors() && //3.执行CAS 获取锁 并修改锁状态 compareAndSetState(0, acquires)) { //4.标记一下 把自己设置为锁的线程持有者 setExclusiveOwnerThread(current); return true; } } //走到这里 就说明进来时第一步获取锁没有成功 //1.还是先判断下当前锁的持有者是不是自己 如果是自己持有 则就是重入 把当前线程state值加1 else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } //都没有返回 那说明真的获取失败 返回false return false; }//如果前面没有获取成功 继续回到最外层的方法中 继续往下走 public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
------------说明 tryAcquire返回false 取反满足条件 继续往下走 进入acquireQueued方法中-------------
先看下addWaiter方法:
//首先进来 是给定了一个独占模式的Node节点 private Node addWaiter(Node mode) { //将当前线程包装成一个Node节点 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; //CAS设置当前节点为尾节点 // 上面已经有 node.prev = pred,加上下面这句,也就实现了和之前的尾节点双向连接了 if (compareAndSetTail(pred, node)) { pred.next = node; //线程入队 可以返回 return node; } } //走到这里 说明尾节点为null 或者有其他线程竞争抢到了锁 enq(node); return node; }// 采用自旋的方式入队 // 之前说过,到这个方法只有两种可能:等待队列为空,或者有线程竞争入队, // 自旋在这边的语义是:CAS设置tail过程中,竞争一次竞争不到,我就多次竞争,总会排到的 private Node enq(final Node node) { for (;;) { Node t = tail; // 之前说过,队列为空也会进来这里 if (t == null) { // Must initialize // 初始化head节点 // 细心的读者会知道原来 head 和 tail 初始化的时候都是 null 的 // 还是一步CAS,你懂的,现在可能是很多线程同时进来呢 if (compareAndSetHead(new Node())) // 给后面用:这个时候head节点的waitStatus==0, 看new Node()构造方法就知道了 // 这个时候有了head,但是tail还是null,设置一下, // 把tail指向head,放心,马上就有线程要来了,到时候tail就要被抢了 // 注意:这里只是设置了tail=head,这里可没return哦,没有return,没有return // 所以,设置完了以后,继续for循环,下次就到下面的else分支了 tail = head; } else { // 下面几行,和上一个方法 addWaiter 是一样的, // 只是这个套在无限循环里,反正就是将当前线程排到队尾,有线程竞争的话排不上重复排 node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
----ok 现在已经将当前线程节点加入队列 继续往下走 看看队列里面该如何操作呢?
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; //无限循环 for (;;) { //拿到当前节点的前节点 final Node p = node.predecessor(); //如果前节点是头节点 那就去重走一遍 tryAcquire 方法获取锁 if (p == head && tryAcquire(arg)) { //获取锁成功 设置头节点为当前线程节点 setHead(node); p.next = null; // help GC failed = false; return interrupted; } //走到这 说明前一个节点不是头节点或者有其他线程抢占了 看*shouldParkAfterFailedAcquire*方法 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
继续往下走 到shouldParkAfterFailedAcquire这个方法。。。。。看名字即可猜出来 应该挂起获取失败的节点
注意 这里需要科普下waitStatus这个值的一些枚举含义:
在AQS中waitstatus有五种值: 1、INITAL 值为0,表示当前没有线程获取锁(初始状态)。 2、SIGNAL 值为-1,后继节点的线程处于等待的状态,当前节点的线程如果释放了同步状态或者被取消会通知后继节点,后继节点会获取锁并执行。 (当一个节点的状态为SIGNAL时就意味着在等待获取同步状态,前节点是头节点也就是获取同步状态的节点) 3、CANCELLED 值为1,因为超时或者中断,结点会被设置为取消状态,被取消状态的结点不应该去竞争锁,只能保持取消状态不变,不能转换为其他状态。 处于这种状态的结点会被踢出队列,被GC回收。 (一旦节点状态值为1说明被取消,那么这个节点会从同步队列中删除) 4、CONDITION 值为-2,节点在等待队列中,节点线程等待在Condition,当其它线程对Condition调用了singal()方法该节点会从等待队列中移到同步队列中。 5、PROPAGATE 值为-3,表示下一次共享式同步状态获取将会被无条件的被传播下去。(读写锁中存在的状态,代表后续还有资源,可以多个线程同时拥有同步状态)
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; // 前驱节点的 waitStatus == -1 ,说明前驱节点状态正常,当前线程需要挂起,直接可以返回true if (ws == Node.SIGNAL) return true; //大于0 就说明当前节点处于取消状态 该状态下的节点不可以竞争锁 只能等待被GC回收 if (ws > 0) { //这里的代码意思就是不断向前获取 直到获取到一个waitStatus<=0的结点 然后next指向当前节点 do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { // 仔细想想,如果进入到这个分支意味着什么 // 前驱节点的waitStatus不等于-1和1,那也就是只可能是0,-2,-3 // 在我们前面的源码中,都没有看到有设置waitStatus的,所以每个新的node入队时,waitStatu都是0 // 正常情况下,前驱节点是之前的 tail,那么它的 waitStatus 应该是 0 // 用CAS将前驱节点的waitStatus设置为Node.SIGNAL(也就是-1) compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } // 这个方法返回 false,那么会再走一次 for 循序, // 然后再次进来此方法,此时会从第一个分支返回 true return false; } // private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) // 这个方法结束根据返回值我们简单分析下: // 如果返回true, 说明前驱节点的waitStatus==-1,是正常情况,那么当前线程需要被挂起,等待以后被唤醒 // 我们也说过,以后是被前驱节点唤醒,就等着前驱节点拿到锁,然后释放锁的时候叫你好了 // 如果返回false, 说明当前不需要被挂起,为什么呢?往后看 // 跳回到前面是这个方法 // if (shouldParkAfterFailedAcquire(p, node) && // parkAndCheckInterrupt()) // interrupted = true; // 1. 如果shouldParkAfterFailedAcquire(p, node)返回true, // 那么需要执行parkAndCheckInterrupt(): // 这个方法很简单,因为前面返回true,所以需要挂起线程,这个方法就是负责挂起线程的 // 这里用了LockSupport.park(this)来挂起线程,然后就停在这里了,等待被唤醒======= private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); } // 2. 接下来说说如果shouldParkAfterFailedAcquire(p, node)返回false的情况 // 仔细看shouldParkAfterFailedAcquire(p, node),我们可以发现,其实第一次进来的时候,一般都不会返回true的,原因很简单,前驱节点的waitStatus=-1是依赖于后继节点设置的。也就是说,我都还没给前驱设置-1呢,怎么可能是true呢,但是要看到,这个方法是套在循环里的,所以第二次进来的时候状态就是-1了。 // 解释下为什么shouldParkAfterFailedAcquire(p, node)返回false的时候不直接挂起线程: // => 是为了应对在经过这个方法后,node已经是head的直接后继节点了。剩下的读者自己想想吧。
AQS解锁操作:
首先也是 调用的sync的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; } ---------------------------------------------------------- // 先进入tryRelease看看 protected final boolean tryRelease(int releases) { //进来先是拿到锁的state值 - 解锁的1,得到c int c = getState() - releases; //判断当前持有锁的线程是不是与记录的线程是一样的 不一样则抛出异常 if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; //如果c为0了 则说明全部都解锁成功 重入锁也全部释放 if (c == 0) { free = true; //设置持有线程为null setExclusiveOwnerThread(null); } setState(c); return free; } ---------------------------------------------------------- // 进入unparkSuccessor方法内部看下是如何走的 private void unparkSuccessor(Node node) { //这里node是头节点 拿到头节点waitStatus判断是否小于0 int ws = node.waitStatus; //小于0 则CAS一下 设置其状态为初始状态 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); }
AQS总结:
最后 来总结下 :
简单来说,ReentrantLock内部是运用了AQS来实现的 AQS内部是怎么实现的呢?三个东西:1.线程的阻塞和解除阻塞,2.state,3.阻塞队列实现.
线程的阻塞和解除阻塞:AQS 中采用了 LockSupport.park(thread) 来挂起线程,用 unpark 来唤醒线程。
state:锁状态值:0:无锁,其余数字有锁,大于1则说明重入
阻塞队列:里面就是FIFO双向链表,Node节点:{pred:前节点,next:后节点,head:头节点,tail:尾节点,thread:当前节点线程}
首先加锁 就是多条线程同时获取锁资源 第一条线程进去时会先判断队列是否有排队 自己的前置节点是不是头节点 如果是则去CAS一下 如果成功则修改掉锁的state值为1,还有把当前持有线程设置为自己,方便后续重入时可以继续获取,也防止其他线程获取,如果不成功 则会进行入队操作,首先会先判断队尾tail节点是否为空,如果不为空,则把自己放到队尾,让队尾的next指向自己,双向链接。如果队尾不为空或者被别人抢先入队,则会进入循环,直到加入队尾。
加入队列后,会判断自己的前节点是不是头节点,如果是则说明可以抢锁,这时候调用CAS抢锁,如果获取失败,则需要判断前节点的等待状态是不是处于-1 也就是正常状态,如果前节点处于-1(取消状态)则会循环往前找,直到找到一个等待状态为-1(正常状态)的节点,并把其设置为自己的前节点,最后一步就是将其park(挂起),等待被前节点线程唤醒。这就是一套完整的加锁流程。