【Java 集合】LinkedBlockingDeque

在开始介绍 LinkedBlockingDeque 之前, 我们先看一下 LinkedBlockingDeque 的类图:

Alt 'LinkedBlockingDeque 类图'

从其中可以看出他直接实现了 BlockingDeque 接口, 而 BlockingDeque 又实现了 BlockingQueue 的接口, 所以它本身具备了队列的特性。
而实现 BlockingDeque 使其在 BlockingQueue 的基础上多了 Deque 的功能。

什么是 Deque 呢?
Deque(双端队列)是一种具有两端插入和删除操作的数据结构, 允许在队列的前端和后端执行高效的操作。
Deque 是“双端队列”(Double-Ended Queue)的缩写, 其特点在于可以在两端同时进行元素的插入和删除操作, 这使得它在许多应用中具有卓越的灵活性和性能。

我们知道队列只能是头进尾出, 而 Deque 2 边都支持进出, 这个特性更加的灵活。

实现了 BlockingDeque 的 LinkedBlockingDeque 其本身具备了以下特点

  1. 不支持 null 元素: 存入 null 元素会抛出异常
  2. 线程安全性: 在内部中通过 ReentrantLock 保证多线程环境下的安全访问
  3. 无界容量: 在不指定容量的情况下, 默认为 Integer.MAX_VALUE, 理论上的无限容量
  4. 阻塞操作: 当 LinkedBlockingDeque 的操作尝试在空队列中执行移除操作或在满队列中执行插入操作时, 它会导致操作的阻塞

1 实现的数据结构

内部的实现结构就是一个链表, 但是为了支持 2 端都可以出入队操作, 所以是一个双向链表。

2 源码分析

2.1 LinkedBlockingDeque 链表节点的定义

我们知道 LinkedBlockingDeque 的底层实现结构就是一个链表, 而链表绕不开的一个概念就是节点, 所以我们先来看一下 LinkedBlockingDeque 的节点定义。

public class LinkedBlockingDeque<E> {/*** 节点类, 用于存储数据*/static class Node<E> {// 存储的内容E item;// 上一个节点的指针Node<E> prev;// 下一个节点的指针Node<E> next;Node(E x) {item = x;}}
}

上面的 Node 就是队列中的节点了, 声明的属性

item: 存储的内容
prev: 上一个节点的指针
next: 下一个节点指针

可以看出 LinkedBlockingDeque 是一个具备双向操作的链表

2.2 LinkedBlockingQueue 持有的属性


public class LinkedBlockingDeque<E> {/** 队列的第一个元素的节点, 也就是头节点*/transient Node<E> first;/** 队列的最后一个元素节点, 也就是尾节点 */transient Node<E> last;/** 当前队列中的元素个数 */private transient int count;/** 队列的大小, 默认为 Integer.MAX_VALUE */private final int capacity;/** 所有操作队列元素节点时使用的锁 */final ReentrantLock lock = new ReentrantLock();// 非空, 也就是数组中重新有数据了, 可以继续取数据了// 当某个线程尝试从当前的队列中获取元素时, 如果数组中没有数据, 会把这个线程放到这个等待条件中// 在另外一个线程中添加元素到数组中, 会唤醒阻塞在这个等待条件中的线程private final Condition notEmpty = lock.newCondition();// 非满, 也就是数组中的数据又没有达到上限了, 可以继续添加数据// 当某个线程尝试向当前的队列添加元素, 但是当前数组已经满了, 会把这个线程放到这个等待条件中// 在另一个线程中从当前队列中获取一个元素时, 会唤醒等待在这个等待队列中的线程private final Condition notFull = lock.newCondition();}

通过上面属性的声明, 可以发现 LinkedBlockingDeque 和 ArrayBlockingQueue 类似, 都是通过一个可重入锁控制并发, 同时通过 2 个 Condition 实现在队列空或满时挂起或唤醒线程。

再来看表示队列容量的 count, 就是简单的用一个 int 表示, 没有用 volatile 修饰, 可以保证线程的可见性吗?
在 LinkedBlockingQueue 容量的大小却是的用 AtomicInteger 来表示, 为什么不是同样简单用一个 int 呢?

这 2 个问题都可以用 Happens-Before 的监视器原则来解释。
在 LinkedBlockingDeque 中只用了一把锁, 同时 count 的修改也只有在线程持有锁的过程中变更, 所以能够保证可见性, 而监视器原则,
确保了同一把锁的解锁 Happens-Before 加锁, 也就是说在 A 线程解锁之前的操作对于 B 线程获取同一把锁后都是可见的。
而 LinkedBlockingQueue 中使用了 2 把锁, 一个用于添加元素, 一个用于移除元素, 所以 2 把锁之间的加锁和解锁不能保证可见性, 所以使用了 AtomicInteger, 利用其内部的 CAS 操作来保证可见性。

2.3 LinkedBlockingQueue 构造函数

public class LinkedBlockingDeque<E> {// 无参构造函数public LinkedBlockingDeque() {// 调用自身指定容量的构造函数, 默认为 int 的最大值this(Integer.MAX_VALUE);}// 指定容量的构造函数public LinkedBlockingDeque(int capacity) {// 指定的容量小于等于 0, 直接抛出异常if (capacity <= 0)throw new IllegalArgumentException();this.capacity = capacity;}// 指定一个集合的构造函数public LinkedBlockingDeque(Collection<? extends E> c) {this(Integer.MAX_VALUE);final ReentrantLock lock = this.lock;// 加锁lock.lock();try {for (E e : c) {// 元素不能为空if (e == null)throw new NullPointerException();// 把当前的数据封装为 Node 节点, 放到了队列的尾部, 添加失败就抛出异常if (!linkLast(new Node<E>(e)))throw new IllegalStateException("Deque full");}} finally {lock.unlock();}}private boolean linkLast(Node<E> node) {// 队列中的元素个数大于等于容量上限了, 直接返回 falseif (count >= capacity)return false;// 获取尾节点Node<E> l = last;// 新增节点的上一个节点为原本的尾节点node.prev = l;// 当前的尾节点等于新增的节点last = node;// 头节点为空, 原本链表没有数据, 新增节点就是头节点if (first == null)first = node;else// 头节点不为空, 原本链表有数据, 那么把原本的尾节点的下一个节点指向新增的节点l.next = node;// 队列中的元素个数 + 1++count;// 队列有数据了, 唤醒阻塞在非空等待条件上的线程notEmpty.signal();return true;}
}

第一个构造函数无参, 那么创建的队列的容量就是 Integer.MAX_VALUE, 也就是理论上的无限容量。

第二个构造函数可以指定队列大小。

第三个构造函数传入一个集合, 这样也会创建出理论无限容量的队列。

同理里面使用了 ReentrantLock 来加锁, 然后把传入的集合元素按顺序一个个放入 items 中。
这里加锁目的不是使用它的互斥性, 而是让修改后的头节点 first 和尾节点 last 的变动, 对其他线程可见, 也就是使用了 ReentrantLock 的监视器原则

2.4 LinkedBlockingDeque 支持的方法

2.4.1 数据入队方法

LinkedBlockingDeque 提供了多种入队操作的实现来满足不同情况下的需求, 入队操作有如下几种:

  1. add(E e) / addFirst(E e) / addLast(E e)
  2. offer(E e) / offerFirst(E e) / offerLast(E e)
  3. offer(E e, long timeout, TimeUnit unit) / offerFirst(E e, long timeout, TimeUnit unit) / offerLast(E e, long timeout, TimeUnit unit)
  4. put(E e) / putFirst(E e) / putLast(E e)

add() 相关的方法


public class LinkedBlockingDeque<E> {public boolean add(E e) {// 调用到添加到尾部的方法addLast(e);return true;}public void addFirst(E e) {// 调用 offerFirst 的方法if (!offerFirst(e))throw new IllegalStateException("Deque full");}public void addLast(E e) {// 调用 offerLast 的方法if (!offerLast(e))throw new IllegalStateException("Deque full");}}

add() 相关的方法都是直接调用 offer() 对应的方法, 比如 add 对应 offer, addFirst 对应 offerFirst, 所以我们只需要看 offer() 方法的实现就可以了。

offer() 相关的方法

public class LinkedBlockingDeque<E> {public boolean offer(E e) {// 调用自身的 offerLast 方法, 添加到队列的尾部return offerLast(e);}public boolean offerFirst(E e) {// 添加的数据不能为空if (e == null) throw new NullPointerException();// 封装为 Node 节点    Node<E> node = new Node<E>(e);final ReentrantLock lock = this.lock;// 获取锁lock.lock();try {// 添加到队列的头部return linkFirst(node);} finally {lock.unlock();}}public boolean offerLast(E e) {// 添加的数据不能为空if (e == null) throw new NullPointerException();// 封装为 Node 节点  Node<E> node = new Node<E>(e);final ReentrantLock lock = this.lock;// 获取锁lock.lock();try {// 添加到队列的尾部return linkLast(node);} finally {lock.unlock();}}private boolean linkFirst(Node<E> node) {// 节点个数大于等于容量上限了, 直接返回if (count >= capacity)return false;// 获取头节点    Node<E> f = first;// 新增的节点的下一个节点为当前的头节点node.next = f;// 当前的头节的设置为新增的节点first = node;// 当前的尾节点为空, 表示原本链表中没有数据, 把新增的节点同时设置为尾节点if (last == null)last = node;else// 为节点不为空, 表示原本链表中有数据了// 那么只需要把 旧的头节点的上一个节点指向新增的节点f.prev = node;// 队列中的元素个数 + 1    ++count;// 唤醒阻塞在非空等待条件中的线程notEmpty.signal();return true;}private boolean linkLast(Node<E> node) {if (count >= capacity)return false;// 获取尾结点    Node<E> l = last;// 新增节点的上一个节点为原本的尾节点node.prev = l;// 当前的尾节点等于新增的节点last = node;// 头节点为空, 原本链表没有数据if (first == null)// 将头节点设置为新增的节点first = node;else// 尾节点不为空, 原本队列有数据了, 把原本尾节点的下一个节点设置为新增的节点l.next = node;// 队列中的元素个数 + 1      ++count;// 唤醒阻塞在非空等待条件中的线程notEmpty.signal();return true;}
}

offer() 相关的方法的实现很简单, 最终就是调用到内部的 linkFirst() 和 linkLast() 方法, 而 2 个就是链表的节点新增操作。

而 offer() 相关的带超时时间的方法就不展开了, 原理和 LinkedBlockingQueue 一样, 利用了 Condition 的 awaitNanos 进行超时等待, 并在外面用 while 循环控制等待时的中断问题。

put() 相关的方法


public class LinkedBlockingDeque<E> {public void put(E e) throws InterruptedException {// 调用到 putLast 方法putLast(e);}public void putLast(E e) throws InterruptedException {// 添加的数据不能为空if (e == null) throw new NullPointerException();// 将当前的数据封装为 Node 节点            Node<E> node = new Node<E>(e);final ReentrantLock lock = this.lock;lock.lock();try {// 阻塞等待 linkLast 成功while (!linkLast(node))// 添加到队列尾部失败的话, 将当前线程放到非满的等待条件, 等待唤醒后, 尝试添加元素到队列notFull.await();} finally {lock.unlock();}}public void putFirst(E e) throws InterruptedException {if (e == null) throw new NullPointerException();Node<E> node = new Node<E>(e);final ReentrantLock lock = this.lock;lock.lock();try {// 阻塞等待linkFirst成功while (!linkFirst(node))// 添加到队列尾部失败的话, 当前线程放到非满的等待条件, 等待唤醒后, 尝试添加元素到队列notFull.await();} finally {lock.unlock();}}
}

lock 加锁后一直阻塞等待, 直到元素插入到队列中。
整体的方法和 offer 方法类似, 前者在当队列已满时, 进入阻塞, 后者在队列已满时, 则是返回。

2.3.2 数据出队方法

同入队的方法一样, 出队也有多种实现, LinkedBlockingDeque 提供了好几种出队的方法, 大体如下:

  1. remove() / removeFirst() /removeLast()
  2. poll() / pollFirst() / pollLast()
  3. poll(long timeout, TimeUnit unit) / pollFirst(long timeout, TimeUnit unit) / pollLast(long timeout, TimeUnit unit)
  4. take() / takeFirst() / takeLast()

remove() 相关的方法


public class LinkedBlockingDeque<E> {public E remove() {// 调用自身的 removeFirst 方法return removeFirst();}public E removeFirst() {// 调用自身的 pollFirst 方法E x = pollFirst();if (x == null) throw new NoSuchElementException();return x;}public E removeLast() {// 调用自身的 pollLast 方法E x = pollLast();if (x == null) throw new NoSuchElementException();return x;}
}

remove() 相关的方法都是调用到对应的 poll() 方法, 在拿到方法的返回值后, 做一层非空的判断。

poll() 相关的方法

public class LinkedBlockingDeque<E> {public E poll() {// 调用到自身的 pollFirst 方法return pollFirst();}public E pollFirst() {final ReentrantLock lock = this.lock;// 加锁lock.lock();try {// 带锁的执行 unlinkFirst 方法return unlinkFirst();} finally {lock.unlock();}}public E pollLast() {final ReentrantLock lock = this.lock;lock.lock();try {// 带锁的执行 unlinkLast 方法return unlinkLast();} finally {lock.unlock();}}private E unlinkFirst() {Node<E> f = first;// 判断当前的头节点, 为空的话, 直接返回 nullif (f == null)return null;// 获取头节点的下一个节点    Node<E> n = f.next;// 获取头节点的数据E item = f.item;// 置空头节点的数据f.item = null;// 把头节点的下一个节点指向自身f.next = f;// 把头节点的指针 first 指向当前头节点的下一个节点first = n;// 头节点的下一个节点为空if (n == null)// 置空尾结点last = null;else// 否则设置头节点的下一个节点的上一个节点为空n.prev = null;// 队列元素个数 - 1    --count;// 唤醒在 notFull 等待队列上的线程notFull.signal();return item;}private E unlinkLast() {Node<E> l = last;// 尾节点为空, 直接返回 nullif (l == null)return null;// 获取尾节点的上一个节点Node<E> p = l.prev;// 获取尾节点的数据E item = l.item;// 置空尾节点的数据l.item = null;// 设置尾结点的上一个节点为自身l.prev = l;// 设置尾节点的指针 last 指向当前尾结点的上一个节点last = p;// 尾结点的上一个节点为空if (p == null)// 置空头节点first = null;else// 设置尾结点的上一个节点的下一个节点为空p.next = null;//   队列元素个数 - 1       --count;// 唤醒在 notFull 等待队列上的线程notFull.signal();return item;}}

poll() 相关的方法的实现很简单, 最终就是调用到内部的 unlinkFirst() 和 unlinkLast() 方法, 而 2 个就是链表的节点删除操作。

而 poll() 相关的带超时时间的方法就不展开了, 原理和 LinkedBlockingQueue 一样, 利用了 Condition 的 awaitNanos 进行超时等待, 并在外面用 while 循环控制等待时的中断问题。

take() 相关的方法

public class LinkedBlockingDeque<E> {public E take() throws InterruptedException {// 调用到自身的 takeFirst 方法return takeFirst();}public E takeFirst() throws InterruptedException {final ReentrantLock lock = this.lock;lock.lock();try {E x;// 通过 unlinkFirst 方法删除头节点, // 删除失败, 即返回结果为 null, 将当前线程阻塞在非空等待条件上// 等待唤醒后重新执行删除while ((x = unlinkFirst()) == null)notEmpty.await();return x;} finally {lock.unlock();}}public E takeLast() throws InterruptedException {final ReentrantLock lock = this.lock;lock.lock();try {E x;// 通过 unlinkFirst 方法删除头节点, // 删除失败, 即返回结果为 null, 将当前线程阻塞在非空等待条件上// 等待唤醒后重新执行删除while ((x = unlinkLast()) == null)notEmpty.await();return x;} finally {lock.unlock();}}}

put() 方法一个套路, 先通过 lock 加锁, 然后 while 循环重试, 失败就 await 阻塞等待, 等待下次唤醒, 直至成功。

2.3.3 获取元素方法

LinkedBlockingDeque 提供了获取元素的方法主要有 2 个 element()peek() 方法, 他们的区别在于, element() 方法在队列为空时会抛出异常, 而 peek() 方法则是返回 null。


public class LinkedBlockingDeque<E> {public E element() {// 调用 getFirst() 方法return getFirst();}public E peek() {// 调用 peekFirst() 方法return peekFirst();}public E getFirst() {// 调用 getFirst 方法结果还是调用到 peekFirst() 方法E x = peekFirst();if (x == null) throw new NoSuchElementException();return x;}public E getLast() {// 调用 getLast 方法结果还是调用到 peekLast() 方法E x = peekLast();if (x == null) throw new NoSuchElementException();return x;}public E peekFirst() {final ReentrantLock lock = this.lock;// 加锁lock.lock();try {// 头节点为空吗 ? 是的话返回 null, 否则返回头节点的数据return (first == null) ? null : first.item;} finally {lock.unlock();}}public E peekLast() {final ReentrantLock lock = this.lock;// 加锁lock.lock();try {// 尾节点为空吗? 是的话返回 null, 否则返回尾节点的数据return (last == null) ? null : last.item;} finally {lock.unlock();}}
}

获取元素前加锁, 防止并发问题导致数据不一致, 然后利用 first 和 last 2 个节点直接可以获得元素。

2.3.4 删除元素方法

public class LinkedBlockingDeque<E> {public boolean remove(Object o) {// 调用自身的 removeFirstOccurrence 方法return removeFirstOccurrence(o);}public boolean removeFirstOccurrence(Object o) {// 从头开始遍历// 需要移除的节点为 null, 直接抛异常if (o == null) return false;final ReentrantLock lock = this.lock;// 加锁lock.lock();try {// 从 first 向后开始遍历比较, 找到元素后调用 unlink 移除for (Node<E> p = first; p != null; p = p.next) {if (o.equals(p.item)) {unlink(p);return true;}}return false;} finally {lock.unlock();}}public boolean removeLastOccurrence(Object o) {// 从尾开始遍历if (o == null) return false;final ReentrantLock lock = this.lock;// 加锁lock.lock();try {// 从 last 向前开始遍历比较, 找到元素后调用 unlink 移除for (Node<E> p = last; p != null; p = p.prev) {if (o.equals(p.item)) {unlink(p);return true;}}return false;} finally {lock.unlock();}}void unlink(Node<E> x) {// 获取删除节点的上一个节点Node<E> p = x.prev;// 获取删除节点的下一个节点Node<E> n = x.next;// 需要删除的节点上一个节点为空, 需要删除的节点为头节点if (p == null) {unlinkFirst();} else if (n == null) {// 需要删除的节点的下一个节点为空, 需要删除对接的为尾节点unlinkLast();} else {// 把需要删除的节点的上一个节点的下一个节点的指针 指向 需要删除的节点的下一个节点p.next = n;// 把需要删除的节点的下一个节点的上一个节点的指针 指向 需要删除的节点的上一个节点n.prev = p;// 置空需要删除节点的数据x.item = null;// 队列中的元素个数 - 1--count;// 唤醒阻塞在 notFull 等待队列上的线程notFull.signal();}}
}

删除元素是从头 / 尾向两边进行遍历比较, 故时间复杂度为 O(n), 最后调用 unlink 把要移除元素的 prev 和 next 进行关联, 把要移除的元素从链中脱离, 等待下次 GC 回收。

3 总结

总体而言, LinkedBlockingDeque 的源码实现通过锁、条件等待和双向链表结构, 保证了在多线程环境中对队列的安全操作。
其本身可以作为一个双端队列使用, 也可以作为一个双端阻塞队列使用, 具有很好的灵活性。

4 转载

【细谈Java并发】谈谈LinkedBlockingDeque

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

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

相关文章

Spring Boot自动装配原理以及实践

了解自动装配两个核心 Import注解的作用 Import说Spring框架经常会看到的注解&#xff0c;它有以下几个作用: 导入Configuration类下所有的bean方法中创建的bean。导入import指定的bean&#xff0c;例如Import(AService.class)&#xff0c;就会生成AService的bean&#xff0…

获取请求体中json数据并解析到实体对象

目录 相关依赖 前端代码 后端代码 测试结果 相关依赖 <dependency><groupId>com.alibaba</groupId><artifactId>fastjson</artifactId><version>1.2.83</version> </dependency> <dependency><groupId>comm…

02 ModBus TCP

目录 一、ModBus TCP 一帧数据格式 二、0x01 读线圈状态 三、0x03读保持寄存器 四、0x05写单个线圈 五、0x06 写单个寄存器 六、0x0f写多个线圈 七、0x10&#xff1a;写多个保持寄存器 八、通信过程 九、不同modbus通信模式的应用场景 一、ModBus TCP 一帧数据格式 其…

fill-in-the-middle(FIM) 实现与简单应用

1 背景 传统训练的 GPT 模型只能根据前文内容预测后文内容&#xff0c;但有些应用比如代码生成器&#xff0c;需要我们给出上文和下文&#xff0c;使模型可以预测中间的内容&#xff0c;传统训练的 GPT 就不能完成这类任务。 传统训练的 GPT 只能根据上文预测下文 使用 FIM…

基于Pytest+Requests+Allure实现接口自动化测试

一、整体结构 框架组成&#xff1a;pytestrequestsallure 设计模式&#xff1a; 关键字驱动 项目结构&#xff1a; 工具层&#xff1a;api_keyword/ 参数层&#xff1a;params/ 用例层&#xff1a;case/ 数据驱动&#xff1a;data_driver/ 数据层&#xff1a;data/ 逻…

玩转大数据19:数据治理与元数据管理策略

随着大数据时代的到来&#xff0c;数据已经成为企业的重要资产。然而&#xff0c;如何有效地管理和利用这些数据&#xff0c;成为了一个亟待解决的问题。数据治理和元数据管理是解决这个问题的关键。 1.数据治理的概念和重要性 数据治理是指对数据进行全面、系统、规范的管理…

MLOps在极狐GitLab 的现状和前瞻

什么是 MLOps 首先我们可以这么定义机器学习&#xff08;Machine Learning&#xff09;&#xff1a;通过一组工具和算法&#xff0c;从给定数据集中提取信息以进行具有一定程度不确定性的预测&#xff0c;借助于这些预测增强用户体验或推动内部决策。 同一般的软件研发流程比…

行为型设计模式(一)模版方法模式 迭代器模式

模板方法模式 Template 1、什么是模版方法模式 模版方法模式定义了一个算法的骨架&#xff0c;它将其中一些步骤的实现推迟到子类里面&#xff0c;使得子类可以在不改变算法结构的情况下重新定义算法中的某些步骤。 2、为什么使用模版方法模式 封装不变部分&#xff1a;模版…

vscode配置node.js调试环境

node.js基于VSCode的开发环境的搭建非常简单。 说明&#xff1a;本文的前置条件是已安装好node.js(具体安装不再赘述&#xff0c;如有需要可评论区留言)。 阅读本文可掌握&#xff1a; 方便地进行js单步调试&#xff1b;方便地查看内置的对象或属性&#xff1b; 安装插件 C…

RouterSrv-DHCP

2023年全国网络系统管理赛项真题 模块B-Windows解析 题目 安装和配置DHCP relay服务,为办公区域网络提供地址上网。DHCP服务器位于AppSrv服务器上。拆分DHCP服务器上的作用域,拆分的百分比为7:3。InsideCli优先从RouterSrv获取地址。配置步骤 安装和配置DHCP relay服务,为办…

AIGC:阿里开源大模型通义千问部署与实战

1 引言 通义千问-7B&#xff08;Qwen-7B&#xff09;是阿里云研发的通义千问大模型系列的70亿参数规模的模型。Qwen-7B是基于Transformer的大语言模型, 在超大规模的预训练数据上进行训练得到。预训练数据类型多样&#xff0c;覆盖广泛&#xff0c;包括大量网络文本、专业书籍…

云原生消息流系统 Apache Pulsar 在腾讯云的大规模生产实践

导语 由 InfoQ 主办的 Qcon 全球软件开发者大会北京站上周已精彩落幕&#xff0c;腾讯云中间件团队的冉小龙参与了《云原生机构设计与音视频技术应用》专题&#xff0c;带来了以《云原生消息流系统 Apache Pulsar 在腾讯云的大规模生产实践》为主题的精彩演讲&#xff0c;在本…

Linux shell编程学习笔记37:readarray命令和mapfile命令

目录 0 前言1 readarray命令的格式和功能 1.1 命令格式1.2 命令功能1.3 注意事项2 命令应用实例 2.1 从标准输入读取数据时不指定数组名&#xff0c;则数据会保存到MAPFILE数组中2.2 从标准输入读取数据并存储到指定的数组2.3 使用 -O 选项指定起始下标2.4 用-n指定有效行数…

21.Servlet 技术

JavaWeb应用的概念 在Sun的Java Servlet规范中&#xff0c;对Java Web应用作了这样定义&#xff1a;“Java Web应用由一组Servlet、HTML页、类、以及其它可以被绑定的资源构成。它可以在各种供应商提供的实现Servlet规范的 Servlet容器 中运行。” Java Web应用中可以包含如下…

人工智能的发展之路:时间节点、问题与解决办法的全景解析

导言 人工智能的发展历程充满了里程碑式的事件&#xff0c;从早期的概念到今天的广泛应用&#xff0c;每个时间节点都伴随着独特的挑战和创新。本文将详细描述每个关键时间节点的事件&#xff0c;探讨存在的问题、解决办法&#xff0c;以及不同阶段之间的联系。 1. 195…

重温经典struts1之自定义转换器及注册的两种方式(Servlet,PlugIn)

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 前言 Struts的ActionServlet接收用户在浏览器发送的请求&#xff0c;并将用户输入的数据&#xff0c;按照FormBean中定义的数据类型&#xff0c;赋值给FormBean中每个变量&a…

Databend 源码阅读: Meta-service 数据结构

作者&#xff1a;张炎泼&#xff08;XP&#xff09; Databend Labs 成员&#xff0c;Databend 分布式研发负责人 drmingdrmer (张炎泼) GitHub 引言 Databend 是一款开源的云原生数据库&#xff0c;采用 Rust 语言开发&#xff0c;专为云原生数据仓库的需求而设计。 面向云架…

利用prometheus+grafana进行Linux主机监控

文章目录 一.架构说明与资源准备二.部署prometheus1.上传软件包2.解压软件包并移动到指定位置3.修改配置文件4.编写启动脚本5.启动prometheus服务 三.部署node-exporter1.上传和解压软件包2.设置systemctl启动3.启动服务 四.部署grafana1.安装和启动grafana2.设置prometheus数据…

第二节TypeScript 基础语法

1、typescript程序由以下几个部分组成&#xff1a; 模块函数变量语句和表达式注释 2、开始第一个typescript程序 创建一个typescript程序&#xff0c;使之输出“hello typescript”&#xff1a; 代码&#xff1a; var message:string "hello typescript" cons…

美颜SDK技术对比,深入了解视频美颜SDK的工作机制

如何在实时视频中呈现更加自然、美丽的画面&#xff0c;而这正是美颜SDK技术发挥作用的领域之一。本文将对几种主流视频美颜SDK进行深入比较&#xff0c;以揭示它们的工作机制及各自的优劣之处。 随着科技的不断进步&#xff0c;美颜技术已经从简单的图片处理发展到了视频领域…