这篇文章给大家分享的是有关AbstractQueuedSynchronizer预热的示例分析的内容。小编觉得挺实用的,因此分享给大家做个参考,一起跟随小编过来看看吧。
// 我不确定有多少人卡在这里
// 我是这么理解的 某个对象在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预热的示例分析”这篇文章就分享到这里了,希望以上内容可以对大家有一定的帮助,让大家可以学到更多知识,如果觉得文章不错,可以把它分享出去让更多的人看到吧!
亿速云「云服务器」,即开即用、新一代英特尔至强铂金CPU、三副本存储NVMe SSD云盘,价格低至29元/月。点击查看>>
免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。
原文链接:https://my.oschina.net/snakevash123/blog/3122908