一.锁
说起AbstractQueuedSynchronizer(传说中的AQS),可能有些同学不知道,但是说到ReentrantLock,CountDownLatch,Seamphore,大家可能用用过吧。他们都是用锁来实现了,而锁里面又分独占锁和分享锁。具体锁的种类请看文章。ReentrantLock是独占锁,而CountDownLatch,Seamphore是分享锁。锁里面还有一个重要的分类:公平锁和非公平锁。java源码系列。
公平锁:线程一个一个排队,确保先来的那个等待的线程最早执行。
非公平锁:不能保证先来的那个等待的线程最早执行。
二.AQS
AQS是一个抽象类,经常被使用的是它的子类Sync,会具体实现AQS里面一些方法。AQS里面主要有三个变量。
private transient volatile Node head; //队列头结点 private transient volatile Node tail; //队列尾 private volatile int state; //同步的状态
用了一个双向的链表来存储当前线程信息。大致示意图如下
原理介绍:
以ReentrantLock为例,他的初始状态state=0,当有线程获取锁的时候,state加1,接着后面还有线程过来获取这个锁,就需要在等待队列里面等待。如果之前的线程释放锁,那么后面等待队列里面的线程就可以获取到锁,执行任务。如果期间有中断,这个线程也会被终止掉。
三.源码实现
这里主要介绍以ReentrantLock为基础介绍AQS。
3.1 ReentrantLock锁初始化
public ReentrantLock() { //默认是new一个非公平的锁,线程之间需要按顺序排队,效率会高一点 sync = new NonfairSync(); }
public ReentrantLock(boolean fair) { //通过true和false来指定是否创建一个公平锁 sync = fair ? new FairSync() : new NonfairSync(); }
3.2ReentrantLock 获取锁
public void lock() { //获取锁 sync.lock(); //默认用的是非公平锁,那我们就先来看看非公平锁的实现 }
final void lock() { if (compareAndSetState(0, 1)) //如果还没有线程获取到资源,就将当前线程设置到独占锁 setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); //如果已经有线程占用了资源,其他线程需要排队 }
public final void acquire(int arg) { //尝试着去获取资源,如果没有获取到,就会把当前线程封装成一个节点放到同步队列里面,如果添加队列成功,就获取队列里面的资源,如果期间发生了中断,就将当前线程中断掉 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); //如果线程在阻塞的过程中发生了中断,自身线程也需要中断 }
protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); }
final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); //获取当前线程 int c = getState(); //获取同步状态 if (c == 0) { //如果锁获取资源 if (compareAndSetState(0, acquires)) { //设置资源的状态 setExclusiveOwnerThread(current); //将当前线程设置为独占模式 return true; } } else if (current == getExclusiveOwnerThread()) { //检查当前线程是独占模式,如果成立的话,这里可以是线程重入 int nextc = c + acquires; //资源增加acquires个 if (nextc < 0) // 资源添加的超出int的范围,抛出越界异常 throw new Error("Maximum lock count exceeded"); setState(nextc); //设置资源的状态 return true; } return false; }
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)) { //就将这个node节点添加到队列的尾部 pred.next = node; return node; } } enq(node); //如果队列为空,就需要初始化这个队列,并将这个ndoe放到队列的尾部 return node; }
private Node enq(final Node node) { //初始队列,并且将node节点入队列 for (;;) { //死循环(自旋) Node t = tail; if (t == null) { // 如果节点为空,就初始化一个空的节点当做队头和队尾 if (compareAndSetHead(new Node())) tail = head; } else { //之前if执行完了,队列里面有了一个空的数据 node.prev = t; //t就是那个之前添加的空的节点 if (compareAndSetTail(t, node)) { //将node放到队尾 t.next = node; return t; //for 循环唯一的出口 } } } }
final boolean acquireQueued(final Node node, int arg) { //获取队列资源,这里面会将获取不到资源的线程阻塞,当有资源可以获取的时候,它会接着执行,但是注意返回的中断的状态 boolean failed = true; //结果是否是失败 try { boolean interrupted = false; //是否发生了中断 for (;;) { final Node p = node.predecessor(); //找到node的前驱节点 if (p == head && tryAcquire(arg)) { //如果前驱节点是head并且获取资源成功 setHead(node); //将node设置为头节点 p.next = null; //断链,破引用 以便系统GC failed = false; //执行成功了 return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && //ndoe前驱节点不是head,需要返回阻塞的标记 parkAndCheckInterrupt()) //如果已经返回了阻塞的标记,则阻塞线程,并且如果返回线程中断的状态 interrupted = true; } } finally { if (failed) //未知原因,执行失败,则取消获取资源 cancelAcquire(node); } }
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { int ws = pred.waitStatus; if (ws == Node.SIGNAL) //如果前驱节点的状态是signal,直接返回true,是线程去阻塞 return true; if (ws > 0) {// 线程是取消状态 do { node.prev = pred = pred.prev; } while (pred.waitStatus > 0); //从后往前过滤取消线程,直到第一个线程不是取消的线程为止 pred.next = node; } else { //如果不是取消状态 我们就需要将前驱节点的状态设置为signal,到下一次执行该方法是,前驱节点就是signal,返回为true compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }
private final boolean parkAndCheckInterrupt() { //中断线程 使用LockSupport.park中断当前线程 LockSupport.park(this); return Thread.interrupted(); //返回的是线程的中断状态,可能这个线程在阻塞的过程中发生过中断 }
小结:到此获取锁已经讲完了。
1.线程先去获取锁,如果可以获取锁,直接返回。
2.如果线程不能获取到锁,就会添加到阻塞队列中,如果队列为空,还需要初始化队列。
3.在线程阻塞的过程中,可能会发生中断,最后可能还需要中断处理一下。
3.3ReentrantLock 释放锁
public void unlock() { 释放锁 sync.release(1); }
public final boolean release(int arg) { if (tryRelease(arg)) { //是否可以释放资源 Node h = head; if (h != null && h.waitStatus != 0) //如果队列头节点不为null并且它的状态不是0 unparkSuccessor(h); //唤醒后继节点 return true; } return false; }
protected final boolean tryRelease(int releases) { //是否可以释放资源 int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) //校验如果当前线程不是独占式,那就有问题了,我们知道,我们前面加入的全部都是独占式的 throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { //如果资源全部放出来了 free = true; setExclusiveOwnerThread(null); //将独占式的线程设置为null } setState(c); //设置资源的状态 return free; }
private void unparkSuccessor(Node node) { int ws = node.waitStatus; if (ws < 0) //如果线程没有被取消,设置前驱node节点的的状态是0 compareAndSetWaitStatus(node, ws, 0); Node s = node.next; if (s == null || s.waitStatus > 0) {//如果后继节点是null或者节点是取消状态 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); //唤醒后继节点 }
小结:到此释放锁已经讲完了。
1.先去看线程能不能获取到释放资源的能力
2.如果可以的话就跳过那些已经取消了的线程(是头节点的后继节点里面的线程)唤醒。
四.总结
本文RentrantLock为骨,讲解了非公平锁的实现,借助了底层了AQS,下面来总结一下大致的流程:
初始状态state=0,当有线程获取锁的时候,state加1,接着后面还有线程过来获取这个锁,就需要在等待队列里面等待。同步队列里面头结点获取资源后,会唤醒后继节点去获取锁,当他获取到锁就会执行自己的任务。如果期间有中断,这个线程也会被终止掉。