JDK1.8 AbstractQueuedSynchronizer的实现分析(学习笔记)
来源:互联网 发布:人工智能第三版答案 编辑:程序博客网 时间:2024/06/08 19:39
lock方法会调用acquire方法,该方法在AQS中实现
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
正常使用一个ReentrantLock 的lock() 方法时,在不能获得所得情况下,该方法是阻塞的,对吧
以上acquire方法拆解一下方法调用
首先tryAcquire(arg),英语不好的我Acquire,百度给出的意思是获得,获取,那么好这里可以给成意思是尝试获得的意思?
如果申请资源失败,则返回false,那么会先调用addWaiter(Node.EXCLUSIVE),这里的Node.EXCLUSIVE代表独占的意思,so这个节点是独占的?
/** * Creates and enqueues node for current thread and given mode. * * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared * @return the new node */ 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) { //如果当前链表有尾节点,就把封装当前线程的节点追加到尾部?这里的尾节点操作也是CAS的 node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } // 如果没有等待的线程(节点列表为null),则初始化头尾节点,该方法是一个自旋的方法 enq(node); return node; }
这里考虑多线程调用的情况,这里的链表不存在线程安全问题,因为对每个头尾节点的操作都是CAS的。详见代码片段里的
compareAndSetHead和compareAndSetTail方法。
/** * Inserts node into queue, initializing if necessary. See picture above. * @param node the node to insert * @return node's predecessor */ private Node enq(final Node node) { for (;;) { Node t = tail; //第一次的时候尾节点肯定是一个 null,则初始话一个空内容节点为头节点,第二次循环时尾节点已经不是null了 if (t == null) { // Must initialize if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; //将node节点的前驱节点设置成尾节点,即先尝试追加到链表的尾部,如果尾节点是t,则将尾节点设置成node,退出循环 if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
所以第一个等待锁的线程A在AQS中的数据结构是这样的?
空节点 -> node(name:A)
看到这里,AQS中对内部链表的头尾操作都是CAS的,所以链表中的节点排序也是完全按照申请锁的顺序排列的。addWaiter方法的操作无非是将线程封装为一个node,追加该节点到链表的尾节点,然后addWaiter 方法返回封装好的node方法,继续调用acquireQueued(node) ,
acquireQueued 方法是一个自旋的方法,假设当前只有一个线程A排队申请锁,目前链表形式为。
空节点 -> node(name:A)
/** * Acquires in exclusive uninterruptible mode for thread already in * queue. Used by condition wait methods as well as acquire. * * @param node the node * @param arg the acquire argument * @return {@code true} if interrupted while waiting */ final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) {//自旋方法 final Node p = node.predecessor(); //获得p的前驱节点,并判断是否为head节点,如果为head节点且成功获取到资源,就将当前线程节点设置成队列的头节点,并返回 if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
这里判断p==head,如果node的前驱节点是head,说明该节点排成了老二,而老二如果成功获取到资源,则变成老大,那么这里老大的起到的是什么作用?
>
我的程序有一把锁在外边阻塞中(sleep),从代码上看一开始tryAcquire方法就成功申请到资源,就不会有他的node维护在这里,就是说他的锁不需要在这里排队?那么后续的unlock是怎样的一个机制?
因为锁在别的线程中持有(state>0),所以这个acquireQueued方法中的tryAcquire会返回false
然后shouldParkAfterFailedAcquire,字面意思如果申请资源失败则判断是否可以挂起线程
/** * Checks and updates status for a node that failed to acquire. * Returns true if thread should block. This is the main signal * control in all acquire loops. Requires that pred == node.prev. * * @param pred node's predecessor holding status * @param node the node * @return {@code true} if thread should block */ private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { //获取节点的前驱节点等待状态 int ws = pred.waitStatus; //如果是SIGNAL状态,返回true,第一次来这个肯定不会成立,因为是node的前驱节点是一个空节点。 //空节点->node(A) if (ws == Node.SIGNAL) /* * This node has already set status asking a release * to signal it, so it can safely park. */ return true; if (ws > 0) { //Node.CANCLED 如果节点是被放弃的(什么情况下会放弃,中断么??) /* * Predecessor was cancelled. Skip over predecessors and * indicate retry. */ do { //一直往前找,把当前节点插入到被取消的节点的前面 node.prev = pred = pred.prev; } while (pred.waitStatus > 0); pred.next = node; } else { /* * waitStatus must be 0 or PROPAGATE. Indicate that we * need a signal, but don't park yet. Caller will need to * retry to make sure it cannot acquire before parking. */ //将前驱节点的状态设置成SIGNAL compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }
至此针对我的测试,这个方法会走compareAndSetWaitStatus(pred, ws, Node.SIGNAL); 分支,所以目前node(name:A)节点的前驱(head节点)的waitStatus状态为 SIGNAL,该方法的第一次调用会返回false
目前针对线程A的链表表示
node(waitStatus:SIGNAL)->node(name:A)
因为shouldParkAfterFailedAcquire返回了false,所以不会调用parkAndCheckInterrupt,那么会进入第二次循环。
node的前驱节点是head 这个是毫无疑问的,但是就是因为获取锁的线程还是一个熊孩子,没有释放资源(state>0)
所以依旧会调用shouldParkAfterFailedAcquire方法,但此时方法内部已经不一样了,这里已经开始满足第一个条件分支了,所以此方法此次会返回true,因为因为shouldParkAfterFailedAcquire返回了true,所以会接着调用parkAndCheckInterrupt()方法
该方法实现如下:
/** * Convenience method to park and then check if interrupted * * @return {@code true} if interrupted */ private final boolean parkAndCheckInterrupt() { LockSupport.park(this);//调用park()使线程进入waiting状态 return Thread.interrupted();//如果被唤醒,查看自己是不是被中断的。 }
LockSupport.park(this); 大神说park()会让当前线程进入waiting状态。在此状态下,有两种途径可以唤醒该线程:1)被unpark();2)被interrupt()。
如果当前线程waiting了,那么就不会有第三次循环了….因为当前线程已经waiting了……
这里阻塞的线程应该是申请锁的线程,LockSupport.park(this);参数的作用是对应的blocker会记录在Thread的一个parkBlocker属性中,通过jstack命令可以非常方便的监控具体的阻塞对象.
如果此时又有一个线程B,按照此逻辑继续申请一把锁,按AQS的处理逻辑会维护成如下形式
node(waitStatus:SIGNAL)->node(name:A)->node(name:B)
假设tryAcquire(arg)依旧失败,state依旧不给释放,熊孩子还不还,那么对于node(name:B) 来说,在acquireQueued中他的前驱肯定不是head节点,所以他只能走shouldParkAfterFailedAcquire方法设置前驱节点的状态,然后调用parkAndCheckInterrupt()方法,去阻塞当前当前申请锁的线程。
同上经历2次自旋后在AQS中链表的维护成如下形式
node(waitStatus:SIGNAL)->node(name:A,waitStatus:SIGNAL)->node(name:B)
这里贴一个大神的总结和图
再来总结下它的流程吧:
调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回;
没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
acquireQueued()使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
由于此函数是重中之重,我再用流程图总结一下:
现在AQS的链表里有三个节点。
node(waitStatus:SIGNAL)->node(name:A,waitStatus:SIGNAL)->node(name:B)
假设一开始长时间阻塞不还锁的线程,时间到了玩够了回家吃饭去,线程中调用了reentrantlock.unlock(); 方法,他玩够了我这边两个兄弟还在那waiting呢,是不是会释放他的线程标志
释放锁的代码也是很简单的
public void unlock() { sync.release(1); }
这里sync是实现AQS的一个内部类Sync的实例,所以本质上还是在调用AQS的功能,看看怎么做的。
/** * Releases in exclusive mode. Implemented by unblocking one or * more threads if {@link #tryRelease} returns true. * This method can be used to implement method {@link Lock#unlock}. * * @param arg the release argument. This value is conveyed to * {@link #tryRelease} but is otherwise uninterpreted and * can represent anything you like. * @return the value returned from {@link #tryRelease} */ public final boolean release(int arg) { if (tryRelease(arg)) {//释放资源 Node h = head; if (h != null && h.waitStatus != 0) //头结点不为null,且头结点是有状态的? unparkSuccessor(h);//唤醒等待队列里的下一个线程 return true; } return false; }
由上文得知,调用AQS的acquire(int args)方法,会调用子类的tryAcquire(int args)实现从而实现对线程状态的重置,那么这里有一个tryRelease(arg) 是不是也是子类的一个实现呢?其实就是调用的子类实现,依旧不去管他的实现
暂且把tryRelease当作释放资源,如果释放成功则将头节点做为参数,调用unparkSuccessor方法。
/** * Wakes up node's successor, if one exists. * * @param node the node */ private void unparkSuccessor(Node node) { //这里参数是头结点。 /* * If status is negative (i.e., possibly needing signal) try * to clear in anticipation of signalling. It is OK if this * fails or if status is changed by waiting thread. */ int ws = node.waitStatus;//判断节点的等待状态 if (ws < 0) //如果当前节点是有状态的,清除状态 compareAndSetWaitStatus(node, ws, 0); //置零当前线程所在的结点状态,允许失败。 /* * Thread to unpark is held in successor, which is normally * just the next node. But if cancelled or apparently null, * traverse backwards from tail to find the actual * non-cancelled successor. */ Node s = node.next; //拿到链表中的第二个节点,本例是node(name:A,waitstatus:SIGNAL) 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); //唤醒线程 }
依据上面的逻辑本例中的node(name:A,waitstatus:SIGNAL) 节点所持有的线程将得到释放。但是这个节点并没有被移除。AQS中的链表结构变成了这样
node(waitstatus:0)->node(name:A,waitstatus:SIGNAL)->node(name:B)
此时线程A将得到唤醒,然后他会继续做final boolean acquireQueued(final Node node, int arg) 方法中的自旋。为方法继续把acquireQueued的代码拿出来再贴一遍
/** * Acquires in exclusive uninterruptible mode for thread already in * queue. Used by condition wait methods as well as acquire. * * @param node the node * @param arg the acquire argument * @return {@code true} if interrupted while waiting */ final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) {//自旋方法 final Node p = node.predecessor(); //获得p的前驱节点,并判断是否为head节点,如果为head节点且成功获取到资源,就将当前线程节点设置成队列的头节点,并返回 if (p == head && tryAcquire(arg)) { setHead(node); p.next = null; // help GC failed = false; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) cancelAcquire(node); } }
此时线程A发现他的前驱节点为head,并且此时再申请资源tryAcquire(arg) ,因为那个熊孩子把资源释放了,所以此时申请资源是成功的,然后会将当前节点设置为链表的头结点,并释放原来的头结点占用的内存。
这样链表结构就成了这个样子:
node(name:A,waitstatus:SIGNAL)->node(name:B)
针对线程A来说acquireQueued 方法已经执行完成,假设我们一直老老实实的等没有通知中断线程,则线程A中的代码行reentrantLock.lock() 方法将返回,一个代码行执行完成,会接着执行下一个代码行,lock() 加锁成功了!那么问题来了(问题和挖掘机没有任何关系)tryAcquire和tryRelease 到底在争抢和释放什么东西?
继续看tryAcquire方法在ReentrantLock类中的实现(公平锁)
/** * Fair version of tryAcquire. Don't grant access unless * recursive call or no waiters or is first. */ protected final boolean tryAcquire(int acquires) { //获得当前线程 final Thread current = Thread.currentThread(); //getState来自AQS,放方法取AQS中的全局变量state 注意是全局 int c = getState(); if (c == 0) { //AQS中默认state值为0,如果为0应该可以证明当前AQS队列里的线程没有修改过state,也就是说没有人持有锁 if (!hasQueuedPredecessors() && compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }
可以发现tryAcquire 方法会先获取AQS中的全局变量,也可以说AQS维护的链表中的所有节点都会去检查state这个变量,所以这个state是一个线程状态,也可以看作是他们在抢的一把锁,如果发现state为0,则代表竞争到资源,并通过CAS的方式设置该值为1,然后设置当前线程为exclusiveOwnerThread,另一个条件因为锁是可重入的啊老铁
这里有一个hasQueuedPredecessors() 方法,参照某大神所说
正如hasQueuedPredecessors的注释所说,该方法的作用是为了避免 “线程闯入”,即对于ReentrantLock来说,即便是state为0时,AQS队列中也可能是有节点的(被取消的,打断的节点)等等,这个时候为了保证AQS队列的公平性,不再尝试加锁,而是返回false,到AQS的队列中去排队。所以,这也是该方法被用在公平锁中的原因。
遗留问题:
1.为什么用链表管理线程
2.节点的状态SIGNAL 主要含义
3.在AQS中的线程如何响应中断,中断策略是什么?
/** waitStatus value to indicate thread has cancelled */ static final int CANCELLED = 1; /** waitStatus value to indicate successor's thread needs unparking */ static final int SIGNAL = -1; /** waitStatus value to indicate thread is waiting on condition */ static final int CONDITION = -2; /** * waitStatus value to indicate the next acquireShared should * unconditionally propagate */ static final int PROPAGATE = -3;
引用:
https://www.cnblogs.com/waterystone/p/4920797.html
- JDK1.8 AbstractQueuedSynchronizer的实现分析(学习笔记)
- JDK1.8 AbstractQueuedSynchronizer的实现分析(上)
- 深度解析Java 8:JDK1.8 AbstractQueuedSynchronizer的实现分析(上)
- 深度解析Java 8:JDK1.8 AbstractQueuedSynchronizer的实现分析(上)
- AbstractQueuedSynchronizer的实现分析
- 深度解析Java 8:AbstractQueuedSynchronizer的实现分析(下)
- AbstractQueuedSynchronizer的实现分析(上)
- AbstractQueuedSynchronizer的实现分析(下)
- AbstractQueuedSynchronizer的实现分析(下)
- AbstractQueuedSynchronizer的实现分析(上)
- AbstractQueuedSynchronizer的实现分析(下)
- java AbstractQueuedSynchronizer的实现分析(独占锁)
- java AbstractQueuedSynchronizer的实现分析(共享锁)
- 深度解析Java8 – AbstractQueuedSynchronizer的实现分析(上)
- 深度解析Java8 – AbstractQueuedSynchronizer的实现分析(下)
- 深度解析Java8 – AbstractQueuedSynchronizer的实现分析(上)
- 深度解析Java8 – AbstractQueuedSynchronizer的实现分析(下)
- 深入分析AbstractQueuedSynchronizer独占锁的实现原理:ReentranLock
- 【干货】system/app 下应用打开关闭飞行模式
- SpringBoot新手入门一直显示Bin注入错误
- 32.Struts2_通过超链接动态加载国际化资源文件
- Linux常用命令
- myeclipse maven项目搭建卡死 GC overhead limit exceeded 问题解决
- JDK1.8 AbstractQueuedSynchronizer的实现分析(学习笔记)
- 表格各行变色
- 关于无法修改材质球的问题
- Web前端开发精品课HTML CSS JavaScript基础教程第四章课后编程题答案
- 解决Eclipse添加新server时无法选择Tomcat 8的问题
- 第二、三章小结
- 想要让游戏用户提高体验度,只需这样做!
- OpenGL Partical System by Transform Feedback
- CSS(二)