【阻塞队列】- ArrayBlockingQueue 的原理-迭代器

文章目录

  • 1. 前言
  • 2. 迭代器
  • 3. Itrs
    • 3.1 参数
    • 3.2 迭代器 Itr
      • 3.2.1 参数
      • 3.2.2 构造器
      • 3.2.3 hasNext
      • 3.2.4 next
      • 3.2.5 remove
      • 3.2.6 shutdown
      • 3.2.7 removedAt
      • 3.2.8 takeIndexWrapped
    • 3.3 doSomeSweeping(tryHandler)
    • 3.4 register
    • 3.5 takeIndexWrapped
    • 3.6 removedAt
    • 3.7 elementDequeued
  • 4. 小结


1. 前言

上一篇文章已经介绍了 ArrayBlockingQueue 的原理,但是最后还是留下了一点尾巴,就是 ArrayBlockingQueue 的迭代器,感觉挺有意思的,就接着来写下文章介绍下里面的迭代器。

系列文章:

  • 【阻塞队列】- 生产者和消费者模式
  • 【阻塞队列】- ArrayBlockingQueue 的原理

2. 迭代器

迭代器(Iterator) 是一种设计模式,也是一种编程概念,用于遍历集合(如数组、列表、字典等)中的元素,而无需暴露集合的内部结构。迭代器提供了一种统一的方式来访问集合中的元素,无论集合的具体实现如何。

在队列中,如果支持内部移除操作(即不在队列头部的移除),并且使用循环数组实现队列,那么有可能会导致多个迭代器中的元素数据不一致。比如:如果某个元素被删除,而迭代器正在遍历这个元素,那么这个迭代器可能会指向一个无效的位置。

所以在 LinkedBlockingQueue 和 ArrayBlockingQueue 等阻塞队列的实现中,都会创建一个用于管理迭代器的内部类,通常被称为迭代器管理器。itrs 的主要职责是维护当前队列的所有迭代器实例,并确保在队列发生变化时(如元素被移除、队列被清空等),能够正确地更新或清除这些迭代器,以避免内存泄漏或迭代器失效。

简单来说就是:使用迭代器管理类来管理所有的迭代器,因为随着 putIndex 和 takeIndex 的移动,有些迭代器可能早已失效了。 所以需要用一个类来管理这些迭代器,及时清除没用的或者已失效的迭代器。

  1. 这个类会记录 takeIndex 回绕到 0 的次数,主要用于跟踪队列的循环次数,以便迭代器可以知道队列是否已经进行了回绕
  2. 当内部元素被移除时,通知所有迭代器。因为如果元素被移除,其他下标的元素就要往前移动一格,所以这时候需要确保迭代器知道队列中的元素已经改变,从而可以相应地更新其状态

为了维护活动迭代器的列表,使用了一个简单的链表,该链表由弱引用到 Itr 对象组成,并且仅在持有队列锁时访问。对于过期迭代器,会使用三种机制来清除过期的迭代器:

  1. 创建新迭代器时,进行 O(1) 检查,查找过时的列表元素
  2. takeIndex 回绕到 0 的时候,检查超过了一次循环,但是还没有使用过的迭代器
  3. 当队列为空的时候,会通知并丢弃所有迭代器

下面就来看下迭代器的管理类 Itrs


3. Itrs

3.1 参数

 /*** Itr 迭代器链表,里面的节点是弱引用,也就是说只要发生 GC 就会被回收掉*/private class Node extends WeakReference<Itr> {// 下一个迭代器节点Node next;Node(Itr iterator, Node next) {super(iterator);this.next = next;}}

Node 是迭代器管理类里面的链表节点,里面的迭代器是一个弱引用,如果这个迭代器仅被弱引用所引用,那么这个对象会被视为可回收的,并在垃圾回收期间被清除。

/*** takeIndex 走了多少圈了*/int cycles = 0;/*** 链表头结点*/private Node head;/*** 记录上一次扫描到哪个位置了,当调用 doSomeSweeping 的时候会清空*/private Node sweeper = null;private static final int SHORT_SWEEP_PROBES = 4;private static final int LONG_SWEEP_PROBES = 16;
  • cycles 表示 takeIndex 走了多少圈了,当 takeIndex 回绕到 0 的时候 cycles 就会 + 1,表示饶了多少圈。
  • head 表示迭代器链表的头结点。
  • sweeper 表示上一次扫描到哪个位置,当迭代器管理类清空失效的迭代器的时候(doSomeSweeping),会从 sweeper 继续扫描,避免每一次都扫描整个链表。
  • SHORT_SWEEP_PROBES 和 LONG_SWEEP_PROBES 表示清除无用迭代器的时候一次性会遍历多少个节点

3.2 迭代器 Itr

3.2.1 参数

 /*** 下一个元素的位置*/
private int cursor;/*** 下一个元素*/
private E nextItem;/*** nextItem 的下标*/
private int nextIndex;/*** 最后一次访问的元素*/
private E lastItem;/*** 最后一次访问的元素的索引位置*/
private int lastRet;/*** takeIndex 的前一个下标*/
private int prevTakeIndex;/*** iters.cycles: 表示当前的队列循环了多少圈* prevCycles: iters.cycles 的前一个值*/
private int prevCycles;/*** 说明当前节点不存在或者找不到*/
private static final int NONE = -1;/*** 说明当前节点被调用 remove 删掉了*/
private static final int REMOVED = -2;/*** 分离模式,当所有索引均为负数或 hasNext 第一次返回 false 时,迭代器会切换到脱离模式,允许可控的断开链接*/
private static final int DETACHED = -3;

首先是 cursor,cursor 就是下一个要访问的节点。就是调用 next() 的时候返回的节点的下一个节点。

nextItem 和 nextIndex 是下一个要访问的元素,和 cursor 搭配使用,当调用 next() 方法的时候会把 cursor 赋值给 nextIndex,同时设置对应的 nextItem。所以你可以理解成,nextItem 和 nextIndex 指向了 next() 方法要返回的元素,而 cursor 会指向这个元素的下一个元素,也就是下依次调用 next() 方法会返回的元素

lastItem 和 lastRet 会记录上一次访问过的下标和元素,prevTakeIndex 是记录 takeIndex 的上一个位置,这个变量是用来判断这个节点是不是处于分离模式。

prevCycles 表示当前的队列前一次循环了多少圈,就是 iters.cycles 的上一个值。

最后就是几个重点的下标标记:

  • NONE:表示这个迭代器已经遍历到尾部了,会设置成 NONE 表示后续没用节点了
  • REMOVED: 表示节点被删掉了
  • DETACHED: 分离模式,表示这个迭代器可以从链表中删掉了,已经是一个无效的链表了

3.2.2 构造器

上面这几个参数倒是看的云里雾里的,下面来看下构造器,看看这几个参数是怎么初始化的。

Itr() {// 设置最后一格索引lastRet = NONE;final ReentrantLock lock = ArrayBlockingQueue.this.lock;// 加锁lock.lock();try {// 如果队列里面没有元素了,就说明这个队列里面没有数据,创建的这个迭代器就是一格没有用的迭代器// 所以这个迭代器设置成分离模式if (count == 0) {cursor = NONE;nextIndex = NONE;prevTakeIndex = DETACHED;} else {final int takeIndex = ArrayBlockingQueue.this.takeIndex;prevTakeIndex = takeIndex;// 设置 nextItem,当前迭代器要读取的下一个元素是 takeIndex 位置的值nextItem = itemAt(nextIndex = takeIndex);// cursor 指针指向 takeIndex 的下一个元素下标cursor = incCursor(takeIndex);if (itrs == null) {// 初始化itrs = new Itrs(this);} else {// 注册到链表节点上itrs.register(this); // in this orderitrs.doSomeSweeping(false);}// 设置 cyclesprevCycles = itrs.cycles;// assert takeIndex >= 0;// assert prevTakeIndex == takeIndex;// assert nextIndex >= 0;// assert nextItem != null;}} finally {// 解锁lock.unlock();}
}final E itemAt(int i) {// 通过下标获取元素return (E) items[i];
}

来拆解下上面的这个方法,下面来看下构造器的逻辑,首先把最后一次访问的下标设置为 NONE,也就是初始化的逻辑,然后就加锁。

 // 设置最后一格索引lastRet = NONE;final ReentrantLock lock = ArrayBlockingQueue.this.lock;// 加锁lock.lock();

首先如果队列里面没用元素了,所以这时候创建出来的迭代器也没啥用了,所以这时候直接把这几个具体的下标设置成废弃的状态,也就是:

 cursor = NONE;nextIndex = NONE;prevTakeIndex = DETACHED;

prevTakeIndex 设置成 DETACHED 模式,就是表示这个迭代器节点可以从链表中分离了,分离模式就是这个意思,在清除的时候会把这个节点给删掉。

然后看下队列里面有节点的情况,就是下面的情况。首先初始化 prevTakeIndex = takeIndexnextIndex = takeIndexnextItem = itemAt(nextIndex)cursor = incCursor(takeIndex),这个 incCursor 是获取 takeIndex 的下一个下标,我们来看下这个 incCursor 方法。

/*** cursor 指向下一个元素的位置* @param index* @return*/
private int incCursor(int index) {// 如果到队列尾部了,从头开始if (++index == items.length)index = 0;// 如果到 putIndex 的位置了,说明当前队列里面 putIndex = (index + 1) % len// 意思就是队列里面没有东西可以读取了,因为生产者从 putIndex 开始生产数据if (index == putIndex)// 设置为 -1index = NONE;// 返回 index 的值return index;
}

这个方法其实就是求出 index 的下一个下标,在里面会判断如果到了队列尾部,就从头开始。而如果 index 的下一个下标是 putIndex,那就说明已经遍历到队列尾部了,这时候 index 设置成 NONE,表示后面没用元素了。最后如果都不符合,就返回 index 的值。

再回到构造器函数中,后续还会初始化 itrs,itrs 应该眼熟吧,这个就是迭代器管理类,如果这个管理类没用初始化,那么就初始化,否则就注册到链表节点上面。

if (itrs == null) {// 初始化itrs = new Itrs(this);
} else {// 注册到链表节点上itrs.register(this); // in this orderitrs.doSomeSweeping(false);
}

最后就设置下 prevCycles = itrs.cycles,到这里就算是初始化构造器了,这里面有一个方法 doSomeSweeping,这个方法就是清除没有用的迭代器的方法,每一次有迭代器加入链表的时候都会调用这个方法去清除一下,这个方法后面会解析的。


3.2.3 hasNext

hasNext 方法就是用来判断下迭代器还有没有下一个节点可以访问,通常迭代器的使用就是 hasNext 和 next 来配合的。

while(itr.hasNext()){var item = itr.next()
}

所以来看下这个方法是怎么实现的吧,下面是源码分析:

/*** 判断迭代器是否有下一个元素可以访问,这个方法的设计目的就是在不加锁的情况下高效判断是否有下一个元素,* 避免在常见的情况下频繁获取锁带来的性能开销*/
public boolean hasNext() {// assert lock.getHoldCount() == 0;// 如果 nextItem 不为空,就代表有元素if (nextItem != null)return true;// 否则就是没有下一个元素了noNext();return false;
}

可以看到上面就是判断 nextItem 这个值是不是空,如果不是空就代表有元素,否则就是没用元素,如果没有元素就调用 noNext 这个方法去处理当前迭代器节点。

/*** 如果当前迭代器遍历到最后一个元素了,没有下一个元素可以遍历,就会调用这个方法*/private void noNext() {// 加锁final ReentrantLock lock = ArrayBlockingQueue.this.lock;lock.lock();try {// assert cursor == NONE;// assert nextIndex == NONE;// 如果不是分离模式,就设置成分离模式if (!isDetached()) {// assert lastRet >= 0;incorporateDequeues(); // might update lastRetif (lastRet >= 0) {// lastRet >= 0 代表迭代器遍历过元素,这时候会记录下最后一个元素lastItem = itemAt(lastRet);// assert lastItem != null;// 设置当前迭代器为分离模式,并从链表中删掉detach();}}// assert isDetached();// assert lastRet < 0 ^ lastItem != null;} finally {// 解锁lock.unlock();}}

当迭代器没有下一个节点,就代表遍历到尾部了,所以这时候就需要调用这个方法来处理这个处于分离状态的迭代器节点。

在这个方法中,首先会加锁,然后再判断下是不是分离模式,这个方法如下所示:

/*** 迭代器是否处于分离状态,分离状态可能表示迭代器不再与数据结构(如队列、列表等)关联,或者处于一种特殊的状态,比如被废弃或不再使用* 这个状态可能用于优化资源管理,比如在某些并发或多线程环境中,标记不再需要的迭代器以便清理* @return*/
boolean isDetached() {// assert lock.getHoldCount() == 1;// 小于 0 就代表是 DETACHED 模式return prevTakeIndex < 0;
}

这个方法中判断是不是分离模式就是通过 prevTakeIndex 去判断的。如果不是分离模式,会设置下 lastRet 和 lastItem,也就是设置最后一次访问等到的节点下标和节点值。

incorporateDequeues,这个方法是用来调整 lastRet、nextIndex、cursor 参数,判断迭代器是不是过期了,因为这三个参数是贯穿迭代器的整个生命周期,也是通过这三个参数实现迭代器的遍历,所以这里就需要去调整这三个参数,然后重新设置下迭代器的状态,下面就来看看这个方法的具体实现逻辑。

/**
* 调整下面几个参数索引,其实就是在迭代器移动的时候调用这个方法去调整索引参数,因为 takeIndex 和 putIndex 是不断变化的,迭代器* 的作用是遍历这个范围里面的元素,所以需要调用这个方法去调整,下面是调用这个方法的三个方法*  1.hasNext 方法*  2.next 方法*  3.remove 方法* 但是前提是这个迭代器不为空并且这个迭代器没有处于分离模式*/
private void incorporateDequeues() {// assert lock.getHoldCount() == 1;// assert itrs != null;// assert !isDetached();// assert count > 0;final int cycles = itrs.cycles;final int takeIndex = ArrayBlockingQueue.this.takeIndex;final int prevCycles = this.prevCycles;final int prevTakeIndex = this.prevTakeIndex;/*** cycles != prevCycles: takeIndex 的圈数发生变化了,那么 takeIndex 肯定是发生了变化* takeIndex != prevTakeIndex:takeIndex 发生变化,往后移动了* 所以核心的逻辑都是在判断 takeIndex 是否发生了变化,因为 takeIndex = prevTakeIndex 的前提下有可能是走了一圈了*/if (cycles != prevCycles || takeIndex != prevTakeIndex) {final int len = items.length;// 计算 takeIndex 移动了多少格long dequeues = (cycles - prevCycles) * len+ (takeIndex - prevTakeIndex);// 检查这几个索引的下标是不是合法的if (invalidated(lastRet, prevTakeIndex, dequeues, len))// 如果不合法,就设置成 REMOVEDlastRet = REMOVED;if (invalidated(nextIndex, prevTakeIndex, dequeues, len))// 如果不合法,就设置成 REMOVEDnextIndex = REMOVED;if (invalidated(cursor, prevTakeIndex, dequeues, len))// 如果不合法,就设置成 takeIndexcursor = takeIndex;// 如果三个指针都小于 0,就代表当前迭代器已经失效了,可以从链表中删掉if (cursor < 0 && nextIndex < 0 && lastRet < 0)detach();else {// 设置参数this.prevCycles = cycles;this.prevTakeIndex = takeIndex;}}
}

在这个方法中,如果 cycles != prevCycles || takeIndex != prevTakeIndex,那么就说明队列发生了增加或者删除节点,为什么要这么判断呢?因为正常来说当队列增加或者删除元素的时候 takeIndex 是会变化的,但是迭代器不是一直都在遍历的,所以 takeIndex 有可能会转了一圈再回到当前下标,所以这里需要判断下 cycles,因为就算 takeIndex 回绕到当前下标,那 cycles 也会和 prevCycles 不同,所以这两个条件需要同时判断。

那么如何调整这几个下标呢?其实这个方法并不算是调整这个下标,而算是判断下这个迭代器还是不是合法的,如果不是,就设置下 lastRet、nextIndex、cursor 这几个参数。

计算这几个迭代器是不是合法的,那么要如何计算呢 ?

  • 首先计算 dequeues,dequeues 是已经取消出队的元素个数,计算方法中 (cycles - prevCycles) * len 是指上此到现在过了多少圈,(takeIndex - prevTakeIndex) 表示这两个 index 之间的差值

    • 要说明下,takeIndex 和 prevTakeIndex 有可能是 prevTakeIndex > takeIndex,也有可能是 prevTakeIndex < takeIndex,所以 takeIndex - prevTakeIndex 有可能是正数,也有可能是负数,可以画个图就能看清楚了
      在这里插入图片描述

    • 按照上面来看,takeIndex 从下标 5 到下标 0,(cycles - prevCycles) = 1,所以最终结果就是 1 * 10 + (0 - 5) = 5,意思就是 takeIndex 移动过程中删掉了 5 个节点

  • 接着就是要判断下 lastRet、nextIndex 和 cursor 还是不是合法的,换句话说,这些下标是不是在 preTakeIndex -> takeIndex 这个范围,如果在就说明这个下标的节点被删掉了,那么这个迭代器就已经没有用了。

  • 可以看到,如果 lastRet 和 nextIndex 是不合法的,那么就会设置成 REMOVED,但是如果 cursor 不合法,那么设置成 takeIndex。那我们直到 takeIndex 是不可能小于 0,的,所以这里的寓意就是如果 lastRet 和 nextIndex 对应的节点都被删掉了,那么标记下这两个下标为 REMOVED,如果 cursor 对应的节点被删掉了,那么重新标记为 takeIndex,从 takeIndex 继续读取。

  • 接下来继续判断,如果 cursor < 0 && nextIndex < 0 && lastRet < 0,就表示迭代器失效了,没办法继续遍历了,这时候调用 detach() 标记当前节点为分离模式,并扫描清空链表中的无用链表。那么什么时候 cursor < 0 呢?就是在阻塞队列关闭的时候。

  • 最后,如果这三个变量有一个不小于 0,更新 prevCyclesprevTakeIndex

那么上面就是这个方法的流程,那么 invalidated 如何检测是否合法的呢?我们知道 prevTakeIndex -> takeIndex 区间的就是被移除的,就是不合法的,所以这个方法就是来检测这几个下标是否在这个范围呢?

/*** 判断 index 这个下标是不是合法的* @param index* @param prevTakeIndex* @param dequeues* @param length* @return*/
private boolean invalidated(int index, int prevTakeIndex,long dequeues, int length) {if (index < 0)// 返回 false,因为小于 0 下面这些就没必要校验了return false;// 这个就是求一下从 prevTakeIndex 到 index 之间有多少个节点int distance = index - prevTakeIndex;if (distance < 0)distance += length;// dequeues 就是 prevTakeIndex 到 takeIndex 的距离// distance 就是 prevTakeIndex 到 index 的距离// 如果 dequeues > distance,就代表 index 在 [prevTakeIndex, takeIndex]// 那已知从 takeIndex 开始的元素节点是合法的,也就是说合法区间是 [takeIndex, putIndex) 或者 [takeIndex, len), [0, putIndex)// 这个 index 显然不在合法区间内,所以返回 true 表示不合法return dequeues > distance;
}

在这个方法中,需要计算从 prevTakeIndex 到 index 的距离,然后跟 dequeues 比较,如果 dequeues > distance,那么 index 在 prevTakeIndex 和 takeIndex 之间,就说明是非法的。
在这里插入图片描述
下面我们来看下,如果 cursor < 0 && nextIndex < 0 && lastRet < 0,调用的这个 detach() 标记节点分离的方法,看看是怎么标记的。

private void detach() {// Switch to detached mode// assert lock.getHoldCount() == 1;// assert cursor == NONE;// assert nextIndex < 0;// assert lastRet < 0 || nextItem == null;// assert lastRet < 0 ^ lastItem != null;if (prevTakeIndex >= 0) {// 这里表示迭代器状态还没有设置成分离状态// assert itrs != null;// 设置分离状态,分离状态意思就是这个节点相当于一个废节点,可以从链表中删掉了prevTakeIndex = DETACHED;// 把这个节点从链表中删掉itrs.doSomeSweeping(true);}
}

这个方法里面的注释就是在判断各个 index 下标条件,首先判断如果 prevTakeIndex >= 0,那么设置 prevTakeIndex = DETACHED 表示节点分离了,同时调用管理类的 doSomeSweeping 方法来删除过期的链表节点。

不过注意下这里面的几个 assert,来看下这几个 assert 是什么意思,这些注释都是 JDK 留下的:

  1. assert lock.getHoldCount() == 1: 表示持有锁
  2. assert cursor == NONE:表示当前迭代器里面没有东西可以访问了
  3. assert nextIndex < 0:表示节点被移除掉了
  4. assert lastRet < 0 || nextItem == null:lastRet < 0 或者 nextItem 找不到
  5. assert lastRet < 0 ^ lastItem != null:lastRet < 0 和 lastItem != null 只能一真一假

好了这个方法就先解析到这,下面来看下 next 方法


3.2.4 next

/*** 迭代器获取下一个元素* @return*/public E next() {// assert lock.getHoldCount() == 0;// 下一个元素final E x = nextItem;// 如果为空,就抛出异常if (x == null)throw new NoSuchElementException();// 加锁final ReentrantLock lock = ArrayBlockingQueue.this.lock;lock.lock();try {// 如果迭代器没有分离,更新迭代器的 lastRet、cursor、nextIndexif (!isDetached())incorporateDequeues();// assert nextIndex != NONE;// assert lastItem == null;// 设置最后一次访问的元素的索引位置为 nextIndexlastRet = nextIndex;final int cursor = this.cursor;if (cursor >= 0) {// 设置 nextIndex 下一次要访问的索引下标为 cursor// 设置 nextItem 下一次要访问的元素nextItem = itemAt(nextIndex = cursor);// 设置 cursor 指针的值// assert nextItem != null;this.cursor = incCursor(cursor);} else {// cursor < 0,就说明 cursor = NONE// 这时候就代表 cursor 指向了 putIndex 的位置,后面没有元素可以读了nextIndex = NONE;nextItem = null;}} finally {// 解锁lock.unlock();}// 返回return x;}

next 方法就是获取迭代器指向的下一个元素,上面的逻辑并不难,我们直接看核心的部分。

if (!isDetached()) 在调用 next 的时候会判断如果当前节点还没有分离,那么调用 incorporateDequeues 来判断和更新几个参数下标,这两个方法在上面的 hasNext 已经解析过了,这里是因为 next 方法会更新 lastRet、nextIndex、cursor,才会调用这个方法的。

然后更新 lastRet = nextIndex,获取下 cursor,接着赋值给 nextIndex,然后 cursor 指向下一个指针位置,相当于:

E x = nextItem;
lastRet = nextIndex;
nextIndex = cursor
nextItem = itemAt(nextIndex)
cursor++;
return x;

所以如果 cursor < 0,就说明 cursor < 0,后续不会有节点了,那么上面的方法中,nextIndex = cursor = NONE,nextItem = null,这个方法还是很好理解的,注意因为设置了 lastRet = nextIndex,所以 itemAt(lastRet) = x


3.2.5 remove

remove 方法会删除当前节点,我们知道上面调用了 next 方法之后 lastRet 会被设置成上一次访问过的节点,注意哈调用 hasNext 是不会设置的,所以 remove 方法需要在 next 方法之后去调用,比如下面的例子。

public class IteratorRemoveExample {public static void main(String[] args) {List<String> list = new ArrayList<>();list.add("Apple");list.add("Banana");list.add("Cherry");list.add("Date");Iterator<String> iterator = list.iterator();while (iterator.hasNext()) {String fruit = iterator.next();if (fruit.equals("Banana")) {iterator.remove();}}System.out.println(list); // 输出: [Apple, Cherry, Date]}
}

那我们就来看下这个 remove 方法的具体逻辑吧。

/*** 迭代器删除节点*/
public void remove() {// assert lock.getHoldCount() == 0;// 加锁final ReentrantLock lock = ArrayBlockingQueue.this.lock;lock.lock();try {// 如果当前迭代器还是可以用的if (!isDetached())// 先更新下 lastRet、nextIndex、cursor 的值,看看需不需要丢弃该分离节点incorporateDequeues(); // might update lastRet or detach// 最后一次访问的节点final int lastRet = this.lastRet;// 设置最后一次访问的节点为 NONE,因为要把这个下标的元素删掉this.lastRet = NONE;// 如果最后一次访问的下标 >= 0,说明有元素可以被删除if (lastRet >= 0) {// 如果当前迭代器还没有设置为分离模式if (!isDetached())// 从数组中删除这个 lastRet 位置的值// 其实删除就是从 lastRet 开始往前删除removeAt(lastRet);else {// 如果迭代器已经是分离模式了final E lastItem = this.lastItem;// assert lastItem != null;// 设置最后一次访问的节点为空this.lastItem = null;// 如果最后一次访问的节点下标是值还是 lastItem,没有被更改if (itemAt(lastRet) == lastItem)// 删除最后一次访问的节点removeAt(lastRet);}} else if (lastRet == NONE)// 没有节点了throw new IllegalStateException();// else lastRet == REMOVED and the last returned element was// previously asynchronously removed via an operation other// than this.remove(), so nothing to do.if (cursor < 0 && nextIndex < 0)// 从链表中删除当前的迭代器detach();} finally {// 解锁lock.unlock();// assert lastRet == NONE;// assert lastItem == null;}
}

还是和 next 的套路一样,isDetached 就表示判断下迭代器是不是被分离了,如果不是,更新下 lastRet、nextIndex、cursor 的值,看看需不需要丢弃该分离节点。

然后获取最后一次访问的节点 lastRet,接着把这个 lastRet 设置为 NONE,如果最后一次访问的下标 lastRet >= 0,说明有元素可以被删除。如果没有节点了,就抛出异常。

当有节点可以被删除的时候,判断下节点有没有处于分离模式,如果不处于就表示可以删除节点,这时候会调用 removeAt(lastRet),这个就是 ArrayBlockingQueue 里面的方法了。在上一篇文章里面已经解析过了。

那如果迭代器已经是分离模式了,那么清空 lastItem,设置为 null,如果最后一次访问的节点下标是值还是 lastItem,没有被更改,就删除最后一次访问的节点。

最后如果 cursor < 0 && nextIndex < 0,说明后面没有节点了,调用 private void detach() 来设置节点为分离状态,同时调用 doSomeSweeping 方法去分离。


3.2.6 shutdown

void shutdown() {// assert lock.getHoldCount() == 1;// cursor 设置为 NONEcursor = NONE;// 如果下一个要访问的下标还不是 REMOVEDif (nextIndex >= 0)// 设置状态为 REMOVEDnextIndex = REMOVED;if (lastRet >= 0) {// 设置最后一次成功访问的元素索引为 REMOVED// 同时把最后一次成功访问的元素设置为 nulllastRet = REMOVED;lastItem = null;}// 设置当前迭代器的状态为 DETACHED,也就是分离状态prevTakeIndex = DETACHED;// 但是这里我们没有设置 nextItem,因为我们这里的关闭指的是关闭迭代器的指针移动// 但是如果这时候调用 next() 还是可以访问到下一个节点值,只是这个 nextItem 是最后一个可访问元素
}

关闭迭代器的方法里面会设置 cursor = NONE,同时更新 nextIndex = REMOVED 和 lastRet = REMOVED,最后设置 prevTakeIndex = DETACHED,也就是设置迭代器为分离状态。 那我们知道当调用这个方法之后这几个属性会被设置成 < 0 的状态,这样在 incorporateDequeues 里面就会判断成功然后被回收掉。


3.2.7 removedAt

/**
* 迭代器调用 remove 的时候会调用这个方法,判断下这个迭代器需不需要从链表中移除掉* @param removedIndex* @return*/
boolean removedAt(int removedIndex) {// assert lock.getHoldCount() == 1;// 如果当前迭代器状态变成 DETACHED 了,就说明可以从链表中移除掉if (isDetached())return true;final int cycles = itrs.cycles; // 迭代器周期数final int takeIndex = ArrayBlockingQueue.this.takeIndex; // 队列当前获取元素的位置final int prevCycles = this.prevCycles; // 上一次更新的周期数final int prevTakeIndex = this.prevTakeIndex; // 上一次更新的获取元素的位置final int len = items.length;// 距离上一次更新 prevCycles 过了多少圈int cycleDiff = cycles - prevCycles;// 如果要删除的节点下标在 takeIndex 之前,那就说明发生循环了if (removedIndex < takeIndex)// 圈数差值 + 1cycleDiff++;final int removedDistance =// 求出来从 prevTakeIndex -> removedIndex 之间的距离(cycleDiff * len) + (removedIndex - prevTakeIndex);// assert removedDistance >= 0;int cursor = this.cursor;if (cursor >= 0) {// 大于等于 0 就表示队列里面还有元素可以被访问// 计算 prevTakeIndex -> cursor 之间的距离int x = distance(cursor, prevTakeIndex, len);if (x == removedDistance) {// 如果这两个距离相等,说明 cursor 指向的位置就是要删除的下标位置if (cursor == putIndex)// 如果指向了 putIndex 这个下标,那么表示后续没有数据可以访问了,把 cursor 设置为 NONE// 因为 putIndex 在队列添加元素的时候会增加,而 cursor 是初始化或者调用 next 方法的时候才会更改// 所以这里是可以等于 putIndex 的// 比如 putIndex .... cursor .... takeIndex// 如果队列一直添加元素,并且也没有调用迭代器去访问元素,那么 putIndex 就会不断增加直到 cursor 的位置this.cursor = cursor = NONE;}// 那如果 prevTakeIndex -> cursor 的距离大于 prevTakeIndex -> removedIndex 之间的距离// 就表示 removedIndex 在 cursor 之前,让 cursor - 1else if (x > removedDistance) {// assert cursor != prevTakeIndex;this.cursor = cursor = dec(cursor);}}// 最后一次访问的元素的位置int lastRet = this.lastRet;if (lastRet >= 0) {// 计算 prevTakeIndex -> lastRet 的距离int x = distance(lastRet, prevTakeIndex, len);// 如果和 removedDistance 相同,就代表上一次访问的节点被移除掉了,设置下标为 REMOVEDif (x == removedDistance)this.lastRet = lastRet = REMOVED;else if (x > removedDistance)// 否则将 lastRet 后退一格this.lastRet = lastRet = dec(lastRet);}// 下一个要访问的元素的下标int nextIndex = this.nextIndex;if (nextIndex >= 0) {// 计算 prevTakeIndex -> nextIndex 的距离int x = distance(nextIndex, prevTakeIndex, len);if (x == removedDistance)// 如果等于 removedDistance,就表示下一个要访问的元素被移除掉了,设置下标为 REMOVEDthis.nextIndex = nextIndex = REMOVED;else if (x > removedDistance)// 如果要删除的元素在 nextIndex 之前,说明这个元素是被访问过的然后在这里被删除了// 将 nextIndex 后退一格this.nextIndex = nextIndex = dec(nextIndex);}// 如果下面这三个指针都 < 0,那么就表示这个迭代器已经没有用了else if (cursor < 0 && nextIndex < 0 && lastRet < 0) {// 设置迭代器模式为 DETACHED,表示可从链表中删掉this.prevTakeIndex = DETACHED;return true;}// 否则就是删不掉return false;
}

迭代器调用 remove 方法的时候会调用 removedAt(lastRet) 方法来处理。在这个方法里面,首先判断下如果当前迭代器状态变成 DETACHED 了,就说明这个节点已经是分离模式了,直接返回 true

既然要删掉某个节点的值,肯定是要更新 cursorlastRetnextIndex

  1. 首先 int cycleDiff = cycles - prevCycles 获取距离上一次更新 prevCycles 过了多少圈
  2. removedIndex < takeIndex 表示如果要删除的节点下标在 takeIndex 之前,那就说明 takeIndex 下标得到下一圈才能赶上 removedIndex,所以 cycleDiff++,然后求出从 prevTakeIndex -> removedIndex 之间的距离,也就是 (cycleDiff * len) + (removedIndex - prevTakeIndex),这行代码逻辑我们之前解析过的。
  3. 对于 cursor,如果 >= 0 就表示队列里面还有元素可以访问,那么计算 prevTakeIndex -> cursor 的距离,然后跟 removedDistance 比较。那如果 x == removedDistance,就代表 cursor 到 prevTakeIndex 的距离和 removedIndex 到 prevTakeIndex 的距离是一样的,这时候 cursor 指向的位置就是要删除的下标。所以如果 cursor == putIndex,就说明后续删除完没有节点了,所以把 cursor 设置为 NONE。如果 cursor 到 prevTakeIndex 的距离 > removedDistance,意思 removedIndex 在 cursor 之前,让 cursor - 1。
  4. 同理更新 lastRetnextIndex
  5. 最后判断 nextIndex 的时候如果 cursor < 0 && nextIndex < 0 && lastRet < 0,就把当前迭代器设置为分离模式,返回 true。

3.2.8 takeIndexWrapped

这个方法当 takeIndex 回绕到 0 的时候会被调用,这个方法里面会判断下当前节点是不是应该从链表中删掉。

/*** 当前节点是不是应该从链表中移除掉,当 takeIndex 绕回到 0 时被调用* @return*/
boolean takeIndexWrapped() {// assert lock.getHoldCount() == 1;// 当前迭代器如果是分离状态,就代表可以从链表移除了if (isDetached())return true;// 如果当前迭代器走的圈数比上一次的圈数要大于 1// 就说明 prevCycles 这个属性至少在 takeIndex 走了一圈的时间内没有被修改// 也就是说这个在这个时间这个迭代器没有调用 next、remove ... 这些方法// 所以这个迭代器就没必要存在了,因为 itrs.cycles 都已经走了好几圈了,每个下标的元素都变了好几轮if (itrs.cycles - prevCycles > 1) {// 清除这个迭代器上面的所有属性状态shutdown();// 返回 true,当前迭代器就不能要了return true;}return false;
}

这个方法中会判断然后迭代器是分离模式了,那么就可以从链表中删掉了。如果 itrs.cycles - prevCycles > 1,就意味着 takeIndex 都走了好几圈了,数组早就不是之前的数组,元素也有可能全不一样了,这时候就没必要再继续遍历了,而是直接调用 shutdown 方法关闭迭代器。


3.3 doSomeSweeping(tryHandler)

这个方法专门用来清空无用的迭代器,在创建迭代器和触发 detach() 函数的时候会触发这个方法去清空。

tryHarder 属性代表这次遍历是不是更彻底的遍历,所谓更彻底的遍历就是一次遍历多少个节点。

/**
* 清空无用的迭代器,在创建迭代器和触发 detach() 函数的时候会触发这个方法去清空* @param tryHarder*/
void doSomeSweeping(boolean tryHarder) {// 是否进行更彻底的清理操作,其实就是一次扫描会扫描多少个节点// 这里如果指向更彻底的清除,就会扫描 16 个节点,否则就扫描 4 个节点// 这里扫描的节点是链表节点,不是说这个 probes 指的是没有用的节点int probes = tryHarder ? LONG_SWEEP_PROBES : SHORT_SWEEP_PROBES;// o: 前一个节点// p: 当前节点Node o, p;// 从上一次扫描的位置开始继续扫描final Node sweeper = this.sweeper;// 标志位,确保在一次完整的扫描过程中,链表只会被从头到尾扫描一次boolean passedGo;// 第一次扫描,或者刚刚完成了一次完整的扫描,这时候就是从头结点开始扫描if (sweeper == null) {// o: 上一轮扫描的最后一个节点,也就是本次扫描的前一个节点// p: 本次扫描的节点o = null;p = head;// 从头开始扫描passedGo = true;} else {o = sweeper;p = o.next;// 不是从头开始扫描passedGo = false;}// probes 是扫描的次数for (; probes > 0; probes--) {if (p == null) {if (passedGo)break;o = null;p = head;passedGo = true;}// 获取迭代器final Itr it = p.get();// 下一个节点final Node next = p.next;// 如果迭代器位空或者迭代器处于分离状态if (it == null || it.isDetached()) {// 重新设置遍历的节点probes = LONG_SWEEP_PROBES;// 把当前节点从链表中摘掉,这里 clear 就是清除 Itr 的引用// Node 节点中的 Itr 是弱引用,当被 GC 的时候 Itr 就会回收,所以这里要把弱引用给设置为空,方便 GCp.clear();p.next = null;// 设置头结点if (o == null) {head = next;if (next == null) {// 链表上面没有节点了,迭代器链表也没必要存在,直接设置为空itrs = null;return;}}else// 中间节点o.next = next;} else {// 正常节点o = p;}// 继续遍历下一个节点p = next;}// 记录下本轮遍历到哪个节点this.sweeper = (p == null) ? null : o;
}

这里面我们挑重点的讲,首先 this.sweeper 表示扫描的位置

  1. sweeper == null 表示是第一次扫描,passedGo 设置为 true 表示从头开始扫描
  2. sweeper != null 表示不是第一次扫描,那么 o 设置为 sweeper,表示上一次扫描的位置,p = o.next 表示当前开始从哪里开始扫描,并且将 passedGo 设置为 false

接下来 for 循环,次数是 probes,probes = tryHarder ? LONG_SWEEP_PROBES : SHORT_SWEEP_PROBES。其实这里 probes 更准确的说是代表了本轮遍历要遍历多少个正常的节点,所谓正常的节点就是没有被 GC 回收的和不是分离状态的节点。

  1. 如果 p == null 并且是从头开始扫描,那么证明这时候链表里面没有迭代器了,直接退出。
  2. 如果 p == null 但不是从头开始扫描,那么代表当前已经遍历到结尾了,所以从头开始遍历。

然后如果当前要处理的迭代器为空(被垃圾回收了),或者迭代器是分离状态,那么重新设置要遍历的节点数,也就是:probes = LONG_SWEEP_PROBES

接下来就要处理这个节点了,处理的方法首先就是调用 p.clear() 方法清空 Itr 的引用,同时设置 p.next = null,然后处理链表,分为头结点和中间节点,这部分比较简单,看注释就行了。

处理完之后接着处理下一个 next 节点。

最后 for 循环处理完之后设置 this.sweeper,就是记录本轮遍历到了哪个节点了。


3.4 register

register 是注册方法,创建迭代器的时候在构造器里面会调用这个方法来把迭代器注册到链表上。

/*** 添加一个迭代器节点到链表中*/
void register(Itr itr) {// assert lock.getHoldCount() == 1;head = new Node(itr, head);
}

3.5 takeIndexWrapped

迭代器也有这个 takeIndexWrapped 方法,而管理类的 takeIndexWrapped 更像是迭代器的上层方法,可以看里面的源码,这个方法就是当 takeIndex 转完一圈变成 0 的时候,就会调用这个方法来遍历链表并处理这些迭代器节点,看看那些节点是需要从链表中删掉的,就删掉

/*** 当 takeIndex 转完一圈变成 0 的时候,就会调用这个方法*/
void takeIndexWrapped() {// assert lock.getHoldCount() == 1;// 圈数 + 1cycles++;// o: 上一个节点// p: 当前节点// 从 head 头结点开始遍历for (Node o = null, p = head; p != null;) {// 获取节点里面的迭代器final Itr it = p.get();// 获取下一个节点final Node next = p.next;// 如果节点里面的迭代器为空,或者这个迭代器已经没用了if (it == null || it.takeIndexWrapped()) {// 清除引用,同时把 p 从链表中摘掉p.clear();p.next = null;if (o == null)head = next;elseo.next = next;} else {o = p;}p = next;}// 没用迭代器节点了,把迭代器置空if (head == null)itrs = null;
}

这个方法我就不多说了,其实就是从 head 节点开始遍历,一个一个节点来判断。最后如果迭代器节点都被删掉了,那么就把这个迭代器管理类设置为空,这里也是一种懒加载的模式,就算说有迭代器才会创建迭代器管理类。


3.6 removedAt

同理这个就是迭代器里面的 removedAt 的上层方法,因为阻塞队列删掉一个元素之后,所有的迭代器节点都需要知道这个元素被删掉了,所以会调用这个方法来遍历所有迭代器节点来更新,判断是否需要更新并删除这个无用的节点。

调用位置:ArrayBlockingQueue#removeAt

/*** 通知迭代器 removedIndex 下标的元素已经被移除了* @param removedIndex*/
void removedAt(int removedIndex) {// 遍历所有迭代器节点for (Node o = null, p = head; p != null;) {final Itr it = p.get();final Node next = p.next;// 1.迭代器被 GC 了// 2.更新下迭代器的几个下标,更新完之后判断下迭代器是不是需要被删掉if (it == null || it.removedAt(removedIndex)) {// 把节点 p 从链表中移除掉// assert it == null || it.isDetached();p.clear();p.next = null;if (o == null)head = next;elseo.next = next;} else {o = p;}p = next;}// 如果头结点都没了,说明链表没有节点了,设置为空,方便 GCif (head == null)   // no more iterators to trackitrs = null;
}

3.7 elementDequeued

调用位置:ArrayBlockingQueue#dequeue

上层 dequeue 其实就是 take,poll … 这些消费者方法会调用的。同理也是当消费者获取节点之后会调用这个方法来通知里面的迭代器去处理。

/*** 当从队列里面删除元素的时候,会调用这个方法* 清空迭代器链表里面那些被 GC 掉或者状态是处于分离状态(detached)*/
void elementDequeued() {// assert lock.getHoldCount() == 1;// 如果队列为空if (count == 0)queueIsEmpty();// 如果 takeIndex = 0,就说明走完一圈了else if (takeIndex == 0)takeIndexWrapped();
}

而当队列为空的时候,会调用 queueIsEmpty,这个方法就是从头开始遍历,清空每一个迭代器节点。

/*** 队列为空的时候会调用这个方法去处理所有迭代器*/
void queueIsEmpty() {// 从 head 开始遍历for (Node p = head; p != null; p = p.next) {Itr it = p.get();// 如果迭代器不为空,就清空这个迭代器的弱引用if (it != null) {p.clear();// 关闭迭代器it.shutdown();}}// 然后设置 head 和 itrs 为空,方便 GChead = null;itrs = null;
}

而如果 takeIndex == 0,就说明走完一圈了,这时候调用 takeIndexWrapped 方法,这个方法前面解析过了。


4. 小结

那么迭代器的逻辑就已经说完了,但是其实里面有一些细节可能没解析的那么准确,如果有错误,欢迎指出交流。






如有错误,欢迎指出!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/891390.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

ARM 汇编基础总结

GNU 汇编语法 编写汇编的过程中&#xff0c;其指令、寄存器名等可以全部使用大写&#xff0c;也可以全部使用小写&#xff0c;但是不能大小写混用。 1. 汇编语句的格式 label: instruction comment label即标号&#xff0c;表示地址位置&#xff0c;有些指令前面可能会有标…

【MySQL】深度学习数据库开发技术:使用CC++语言访问数据库

**前言&#xff1a;**本节内容介绍使用C/C访问数据库&#xff0c; 包括对数据库的增删查改操作。 主要是学习一些接口的调用&#xff0c; 废话不多说&#xff0c; 开始我们的学习吧&#xff01; ps:本节内容比较容易&#xff0c; 友友们放心观看哦&#xff01; 目录 准备mysql…

华为配置 之 RIP

简介&#xff1a; RIP&#xff08;路由信息协议&#xff09;是一种广泛使用的内部网关协议&#xff0c;基于距离向量算法来决定路径。它通过向全网广播路由控制信息来动态交换网络拓扑信息&#xff0c;从而计算出最佳路由路径。RIP易于配置和理解&#xff0c;非常适用于小型网络…

1.GPU简介及英伟达开发环境配置

前言 This book shows how, by harnessing the power of your computer’s graphics process unit (GPU), you can write high-performance software for a wide rangeof applications.Although originally designed to render computer graphics ona monitor (and still used…

电脑cxcore100.dll丢失怎么办?

电脑运行时常见问题解析&#xff1a;应对DLL文件丢失、文件损坏与系统报错的实用指南 在数字时代&#xff0c;电脑已成为我们工作、学习和娱乐不可或缺的工具。然而&#xff0c;正如任何精密机械都可能遇到故障&#xff0c;电脑在运行过程中也难免会遇到各种问题&#xff0c;如…

【无线传感网】时间同步技术

文章目录 时间同步模型时钟模型1. 节点本地时钟模型2. 节点逻辑时钟模型 通信模型1. 单向报文传递2. 双向报文交换3. 广播参考报文4. 参数拟合技术 时钟同步的误差来源 时间同步协议时钟同步的类别1. 时钟速率同步与偏移同步2. 同步期限&#xff1a;长期同步与按需同步3. 同步范…

C# 实用工具分享(1)

大家好&#xff0c;今天分享一些在开发过程中比较实用的工具。 首先在软件开发的过程中不可避免的要使用截图这样的功能&#xff0c;以前这样的功能我自己也是选择开发出新功能。但是自己开发还是非常费时费力的&#xff0c;并且效果也不一定特别好。 于是我找到了一个现成的…

积分图(Integral Image)与均值滤波的快速实现

积分图&#xff08;Integral Image&#xff09;也称为求和图&#xff08;Summed Area Table&#xff09;&#xff0c;是一种用于快速计算图像中任意矩形区域像素值总和的技术。 基本概念 积分图的每个位置(i, j)存储的是从图像左上角(1, 1)到当前位置(i, j)所有像素值的累积和…

curl+openssl 踩坑笔记

curl编译&#xff1a;点击跳转 踩坑一 * SSL certificate problem: unable to get local issuer certificate * closing connection #0 curl: (60) SSL certificate problem: unable to get local issuer certificate More details here: https://curl.se/docs/sslcerts.html …

[AI] 深度学习的“黑箱”探索:从解释性到透明性

目录 1. 深度学习的“黑箱”问题&#xff1a;何为不可解释&#xff1f; 1.1 为什么“黑箱”问题存在&#xff1f; 2. 可解释性研究的现状 2.1 模型解释的方法 2.1.1 后置可解释性方法&#xff08;Post-hoc Explanations&#xff09; 2.1.2 内在可解释性方法&#xff08;I…

python-Flask:SQLite数据库路径不正确但是成功访问到了数据库,并对表进行了操作

出现了这个问题&#xff0c;就好像是我要去找在南方的人&#xff0c;然后我刚好不分南北&#xff0c;我认为的方向错了&#xff0c;实则方向对了。 在我针对复盘解决&#xff1a;sqlite3.OperationalError: unrecognized token: “{“-CSDN博客这个内容的时候&#xff0c;又出现…

对称密码算法(分组密码算法 序列密码算法 密码杂凑算法)中的基本操作

对称密码算法(分组密码算法 序列密码算法 密码杂凑算法)中的基本操作 相比非对称加密算法&#xff0c;对称加密算法因为加解密效率较高&#xff0c;因而在日常使用中更加广泛。为了让大家更加熟悉常见的对称加密算法&#xff0c;本文列举出了对称密码算法设计中经常用到的13种基…

大数据治理,数字化转型运营平台建设方案(PPT完整版)

1、大数据治理整体运营思路 2、数据资产定义及流程规范 3、治理规范及质量管控 4、质量考核标准及提升方案 软件全套资料部分文档清单&#xff1a; 工作安排任务书&#xff0c;可行性分析报告&#xff0c;立项申请审批表&#xff0c;产品需求规格说明书&#xff0c;需求调研计划…

专题十四——BFS

目录 一BFS解决FloodFill算法 1图像渲染 2岛屿数量 3岛屿的最大面积 4被环绕的区域 二BFS解决蛋源最短路径问题 1迷宫中离入口最近的出口 2最小基因变化 3单词接龙 4为高尔夫比赛砍树 三BFS解决多源最短路径问题 1 01矩阵 2飞地的数量 3地图中的最高点 4地图分…

DMDRS部署:搭建DM8-DM8数据同步

一、部署要求 1.1 硬件要求 DMDRS服务描述源DMDRS 内存要求至少2GB的内存空间。推荐配置4GB及以上的内存空间。 源DMDRS对内存空间的需求主要与装载的并发数相关。当内存空间配置低于2GB时&#xff0c;可以调整装载的线程数来降低源DMDRS对内存空间的需求。 磁盘要求至少10GB…

仓颉笔记——windows11安装启用cangjie语言,并使用vscode编写“你好,世界”

2025年1月1日第一篇日记&#xff0c;大家新年好。 去年就大致看了一下&#xff0c;感觉还不错&#xff0c;但一直没上手&#xff0c;这次借着元旦的晚上安装了一下&#xff0c;今年正式开动&#xff0c;公司众多的应用国产化正等着~~ 第一步&#xff1a;准备 官网&#xff1a;…

datalist的作用?怎么用的?

在 HTML 中&#xff0c;<datalist> 元素用于为 <input> 元素提供一个可选项列表&#xff0c;帮助用户通过预定义的选项进行快速选择。它是一个增强输入体验的功能&#xff0c;类似于自动完成&#xff08;autocomplete&#xff09;&#xff0c;但与传统的 <selec…

Cocos2dx Lua绑定生成中间文件时参数类型与源码类型不匹配

这两天维护的一个项目&#xff0c;使用arm64-v8a指令集编译时遇到了报错&#xff0c;提示类型不匹配&#xff0c;具体报错的代码【脚本根据C源文件生成的中间文件】如下&#xff1a; const google::protobuf::RepeatedField<unsigned long long>& ret cobj->equi…

1、ELK的架构和安装

ELK简介 elk&#xff1a;elasticsearch logstash kibana&#xff0c;统一日志收集系统。 elasticsearch&#xff1a;分布式的全文索引引擎的非关系数据库&#xff0c;json格式&#xff0c;在elk中存储所有的日志信息&#xff0c;架构有主和从&#xff0c;最少需要2台。 …

常用的数据库类型都有哪些

在Java开发和信息系统架构中&#xff0c;数据库扮演着存储和管理数据的关键角色。数据库种类繁多&#xff0c;各有特色&#xff0c;适用于不同的应用场景。 1. 关系型数据库&#xff08;RDBMS&#xff09;&#xff1a; • 关系型数据库是最为人熟知的数据库类型&#xff0c;数据…