AbstractQueuedSynchronizer预热的示例分析
这篇文章给大家分享的是有关AbstractQueuedSynchronizer预热的示例分析的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。
成都创新互联主营安庆网站建设的网络公司,主营网站建设方案,重庆APP开发公司,安庆h5微信平台小程序开发搭建,安庆网站营销推广欢迎安庆等地区企业咨询
核心方法预热
// 我不确定有多少人卡在这里 // 我是这么理解的 某个对象在jvm当中 是用一块数据来描述对象的所有信息 // 那么问题来了 如果我要设置某个对象的字段 通常的方法 对象引用.setXXXField(xxx)这个是通常的方法 // 还有一种比较特别的 unsafe提供的 unsafe.objectFieldOffset获取某个字段的偏移量 可以理解为存储信息的地址 // 获得了偏移地址之后 就可以使用 unsafe.compareAndSwapObject来原子的设置某个对象的字段 // 就是说 绕过通用的流程 直接修改相关数据了 顺带而且是原子性的 // 可以理解为玩游戏用外挂直接修改内存这种场景 headOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("head")); unsafe.compareAndSwapObject(this, headOffset, expect, update); tailOffset = unsafe.objectFieldOffset(AbstractQueuedSynchronizer.class.getDeclaredField("tail")); unsafe.compareAndSwapObject(this, tailOffset, expect, update); /** * 独占式获取同步状态,忽略线程的打断。 * 获取同步状态的逻辑是由重写的模板方法tryAcquire来实现的。 * 如果获取同步状态成功,则方法就直接返回。 * 否则,线程就会入队,一直会处于阻塞或者自旋,直到重复尝试tryAcquire成功。 * 该方法就是接口Lock#lock的实现。 * (从方法的介绍上面理解,就是说,这个接口直接的效果就是,获取同步成功,线程就从这个方法继续执行下去,如果不成功; * 那么内部会经过一系列复杂的逻辑计算,直接体现就是线程不会继续执行下去,就一直处于这个方法内部。不执行下去的原因是:线程可能处于自旋或者阻塞。) * @param arg 同步状态参数 透传进tryAcquire并且不响应终端或者其他情况(超时) * * 由两种判断逻辑 * 1. tryAcquire(arg) -> 返回 * 2. tryAcquire(arg) -> addWaiter(Node.EXECLUSIVE) -> acquireQueued(lastValue, arg) -> 返回并且可能会中断线程 * * addWaiter(Node node) 入队 * acquireQueued(final Node node, int arg) 自旋或者阻塞 * * 这个方法就是把整个流程已经写死了,必定会经过这么几个步骤。 * 唯一可以影响该方法中的流程,只能是模板方法tryAcquire,它的返回与否,导致流程的走向。 * 把自旋或者阻塞安排在if的条件语句中 会令人初步一看会感觉非常难受。(大神可以这么用,我们平时还是少用)。 */ public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } // 老老实实的我,一般会这么写 见笑见笑 public final void acquire(int arg) { // 尝试获取同步状态 if (tryAcquire(arg)) { return; } else { // 先入队 这里会有一个死循环 Node newNode = addWaiter(Node.EXCLUSIVE); // 再自旋获取同步状态 或者阻塞 这里也会有死循环 boolean shouldCurrentThreadInterrupted = acquireQueued(newNode, arg); // 再判断是否需要线程中断 if (shouldCurrentThreadInterrupted) { selfInterrupt(); } } } // 接下来看看这个模板方法的介绍 /** * 尝试独占式获取同步状态。 * 该方法需要查询对象当前状态,判断同步状态是否符合预期。 * (我的理解就是,需要自己实现自己的逻辑,判断自己所要实现的逻辑是否符合自己的预期。记住是独占模式) * * 该方法经常再线程执行同步时被调用。 * 如果方法返回失败,那么线程就应该入队了,即使线程还没做好入队准备。 * (这里的意思就是说,线程在竞争锁之前,最好做好充足的准备工作,也就是前置逻辑要执行完,比如各种初始化判断。加锁之后就应该是确确实实的逻辑操作了,最好不要加完锁之后,又去判断各种前置业务逻辑操作。这个就是我理解的大师所要阐述的最佳实践。) * 入队的线程只能等待别人释放之后唤醒。 * 一般前置方法就是为了实现Lock#tryLock这个。 * * 默认实现式UnsupportedOperationException异常。 * * @param arg 请求参数。 * 一般这个值是方法唯一的参数,或者保存于条件等待中。 * 所以不建议为这个值赋予更多其他含义。 * (我认为这里的意思是,这个值不要和业务中的某个条件或者流程挂钩,让值单纯的标识同步状态就好了。) * * @return true加锁成功。 * @throws IllegalMonitorStateException 如果获取同步时发现同步器处于一个不正确的状态时, * 那么就必须抛出这个异常,目的时为了同步器逻辑正确。 * (我的理解,同步器状态很重要,必须严肃对待,因为一旦某个过程状态不正确,后续的业务逻辑可能会发生各种不可知的结果,并且,debug起来非常麻烦,因为业务逻辑可能正确,原因是同步状态的出错。这种是很隐晦的。也就是说,一旦碰到IllegalMonitorStateException,个人认为最好中断运行,排错。即使开发者认为这个错误不重要。你都已经自己实现锁的逻辑了,任何一点小的逻辑失误,都会造成不可预估的结果。千里之堤毁于蚁穴啊。) * @throws UnsupportedOperationException 如果独占模式不支持抛异常 */ protected boolean tryAcquire(int arg) { throw new UnsupportedOperationException(); } // 入队操作 /** * 创建队列,并且把当前线程包装一下,指定某个节点模式,入队。 * * @param mode Node.EXCLUSIVE 独占, Node.SHARED 共享 * @return 新的节点 */ 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; // 队尾有两种情况 // 1 null 表示队列还没有初始化 初始化在enq(node)中 // 2 != null 表示队列初始化了 那么尝试快速添加队尾这个操作 我认为就是优化操作了 // (老老实实的我,一般并不会这么写,因为我比较稳妥。) // (其实优化操作,理论上来说,可以不用的。) // compareAndSetTail()这个原子性的操作 防止并发 // 并发操作的特点就是,随时随地都可能发生几个线程同时执行,所以,并发点,尽量条件简单点,如果业务条件够复杂,一定要拆,而且要分优先级的。不然,动态变化的条件加上锁,噩梦。 if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { // 入队操作只需要建立一个尾链接就可以 pred.next = node; return node; // 注意 这里返回的是新的节点 } } enq(node); // 这里方法返回的是节点前置的节点 但是没有使用 在唤醒流程中会复用这个方法 return node; } // 完整的入队流程逻辑 /** * 入队操作,一定要先初始化队列。 * (死循环确保一定会入队成功,我对死循环的理解是,单线程不要用死循环,多线程可以适量的用,主线程不要用,非要用时情愿开个线程计算,等它计算结束再拿那个结果也可以。总结起来,能不用就不用,即使要用,千万别忘记了,自己在干什么。建议在自己精力最旺盛的时候,写带有死循环的逻辑。) * @param node 入队节点 * @return 返回前置节点 */ private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // 队列初始化 // 原子性的设置头 这里注意这个head节点 这个head指向的node是一个空的node,里面没有node的关键数据的 if (compareAndSetHead(new Node())) tail = head; } else { // 双向队列 尝试把当前节点的头设置为原本队尾那个 只要下面的cas队列设置好那就操作成功 不行再循环再来 node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t; } } } } /** * 设置队列首节点 (因为是双向,队首的前驱是null,这个null是为了释放节点的。) * 该方法仅仅只被同步器获取。 * null的目的是为了GC也为了不必要的信号释放遍历。 * * @param node 设置队首 */ private void setHead(Node node) { head = node; node.thread = null; node.prev = null; } // 自旋 /** * 独占不响应中断模式的线程获取同步方法。 * 条件等待也使用该方法。 * * @param node 节点 * @param arg 获取同步参数 * @return true 如果等待时线程被打断 */ 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; // 线程已经被打断 } } finally { if (failed) cancelAcquire(node); } } /** * 当一个节点获取同状态失败时,检查并且更新它的状态。 * 返回true,那么线程需要被阻塞。 * 在所有的获取同步循环中,这个是最重要的信号控制。 * 前置条件是前置节点确切的是节点的前置节点。 * * @param pred 带有状态的前驱节点 * @param node 节点 * @return 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 { /* * 等待状态必须是0或者是传播状态(-3)。 * 仅需要一个信号,而并不需要阻塞。(应该是共享模式下的逻辑。) * 调用者需要重新确保当前线程在阻塞之前是否需要获取同步状态。 */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; } /** * 阻塞当前线程。恢复后检测线程是否被中断了。 * * @return true} if interrupted */ private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted(); }
感谢各位的阅读!关于“AbstractQueuedSynchronizer预热的示例分析”这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,让大家可以学到更多知识,如果觉得文章不错,可以把它分享出去让更多的人看到吧!
分享文章:AbstractQueuedSynchronizer预热的示例分析
分享链接:http://hbruida.cn/article/gphcdg.html