DelayQueue详解

news2024/10/9 0:45:32

目录

  • DelayQueue详解
    • 1、DelayQueue简介
    • 2、DelayQueue适用场景
    • 3、DelayQueue继承体系
    • 4、DelayQueue构造函数
    • 5、DelayQueue数据结构
      • DelayQueue类的属性注释:
      • DelayQueue使用示例
      • Delayed接口的作用
    • 6、DelayQueue的`put`方法
    • 7、DelayQueue的`take`方法
    • 8、DelayQueue的`poll`方法
      • 补充:带参的`poll(long timeout, TimeUnit unit)`方法
    • 9、DelayQueue的`take`和`poll`方法区别
    • 10、DelayQueue的`take`方法的原理

DelayQueue详解

1、DelayQueue简介

DelayQueue 也是 Java 并发包(java.util.concurrent)中的一个特殊队列,用于在指定的延迟时间之后处理元素。

DelayQueue的一些关键特性:

  • 延迟元素处理:只有当元素的延迟时间到期时,元素才能被取出。使用 take 方法会阻塞直到有元素到期。
  • 无界队列:DelayQueue 是一个无界队列,这意味着它可以包含任意数量的元素(太多可能内存溢出)。
  • 元素排序:DelayQueue 中的元素按到期时间排序,最先到期的元素最早被取出。
  • 阻塞操作:take 方法会阻塞直到有元素到期,而 poll 方法可以在指定的时间内等待。

2、DelayQueue适用场景

DelayQueue 通常用于需要在未来某个时间点执行任务的场景。

  • 定时任务调度:可以用于实现定时任务调度系统,任务在特定的时间点被执行。
  • 缓存过期:实现缓存系统中的过期机制,当缓存项的过期时间到达时,将其从缓存中移除。
  • 限流控制:在某些系统中用于限流,限制某一操作在指定时间段内的执行频率。

我在生产上使用过这个队列,主要是做延时执行任务。
当时场景是一个工作流审批节点创建时的回调方法中需要根据某些条件自动通过某些人的审批操作,
由于是在审批节点创建时的回调方法中,此时节点还没有初始化完成,所以就利用异步的线程池+DelayQueue来完成延时任务触发。

3、DelayQueue继承体系

public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
    implements BlockingQueue<E>

在这里插入图片描述
可以看到DelayQueue实现了BlockingQueue接口和Delayed接口。 实现BlockingQueue接口说明支持阻塞和线程安全。
DelayQueue<E extends Delayed> 泛型,表明DelayQueue存储的元素必须继承或者实现Delayed接口。

4、DelayQueue构造函数

空参构造

public DelayQueue() {}

带集合参数的构造函数

public DelayQueue(Collection<? extends E> c) {
        this.addAll(c);
    }

5、DelayQueue数据结构

DelayQueue类的属性注释:

public class DelayQueue<E extends Delayed> extends AbstractQueue<E> implements BlockingQueue<E> {

    // 一个用于控制并发访问的可重入锁
    private final transient ReentrantLock lock = new ReentrantLock();

    // 用于存储队列元素的优先级队列
    private final PriorityQueue<E> q = new PriorityQueue<E>();

    // 指向当前等待获取队列头部元素的线程
    private Thread leader = null;

    // 一个条件对象,用于管理队列元素的可用性
    private final Condition available = lock.newCondition();

}

可以看到DelayQueue内部使用PriorityQueue存储元素,所以DelayQueue的数据结构本质上也是数组存储的小顶堆。

DelayQueue使用示例

场景:
有三只狗分别叫秀逗,四眼,二哈。
它们都被关在笼子里,现在到了吃饭时间,需要等2秒后把二哈先放出来,5秒后把四眼放出来,8秒后把秀逗放出来。

import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

public class TestA {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个DelayQueue
        DelayQueue<DelayedDog> delayQueue = new DelayQueue<>();

        // 插入元素,设置不同的延迟时间
        delayQueue.put(new DelayedDog("秀逗", 8000));
        delayQueue.put(new DelayedDog("四眼", 5000));
        delayQueue.put(new DelayedDog("二哈", 2000));

        // 打印队列内容,仅仅是插入顺序
        System.out.println("DelayQueue: " + delayQueue);

        // 依次取出元素
        while (!delayQueue.isEmpty()) {
            DelayedDog dog = delayQueue.take();
            System.out.println("开门放狗: " + dog);
        }

    }
}


// 实现 Delayed 接口
class DelayedDog implements Delayed {
    private String name;        // 狗的名字
    private long delayTime;     // 延迟时间,以毫秒为单位
    private long expire;        // 到期时间点,以毫秒为单位

    // 构造方法,传入狗的名字和延迟时间(毫秒)
    public DelayedDog(String name, long delayTimeInMillis) {
        this.name = name;
        this.delayTime = delayTimeInMillis;
        // 计算到期时间点,当前时间加上延迟时间
        this.expire = System.currentTimeMillis() + delayTime;
    }

    // 获取剩余延迟时间,以给定时间单位表示
    @Override
    public long getDelay(TimeUnit unit) {
        // 计算剩余时间,单位为毫秒
        long remainingTime = expire - System.currentTimeMillis();
        // 将剩余时间转换为指定单位
        return unit.convert(remainingTime, TimeUnit.MILLISECONDS);
    }

    // 比较两个 DelayedDog 对象的顺序
    @Override
    public int compareTo(Delayed other) {
        // 强制转换为 DelayedDog 类型
        DelayedDog otherDog = (DelayedDog) other;
        // 比较两个对象的到期时间点,返回比较结果
        return Long.compare(this.expire, otherDog.expire);
    }

    // 返回 DelayedDog 对象的字符串表示形式
    @Override
    public String toString() {
        return "DelayedDog{" +
                "name='" + name + '\'' +
                ", delayTime=" + delayTime +
                '}';
    }
}


运行结果:

DelayQueue: [DelayedDog{name='二哈', delayTime=2000}, DelayedDog{name='秀逗', delayTime=8000}, DelayedDog{name='四眼', delayTime=5000}]
开门放狗: DelayedDog{name='二哈', delayTime=2000}
开门放狗: DelayedDog{name='四眼', delayTime=5000}
开门放狗: DelayedDog{name='秀逗', delayTime=8000}

Delayed接口的作用

Delayed 接口在 Java 中用于表示具有延迟功能的对象,这些对象在某个特定时间点之前是不可用的。
实现该接口的对象可以用于构建像 DelayQueue 这样的延迟队列,其元素在到期时间之前是不可访问的。

Delayed 接口定义

public interface Delayed extends Comparable<Delayed> {
    long getDelay(TimeUnit unit);
}

作用和功能

定义延迟时间:
getDelay(TimeUnit unit) 方法返回当前对象的剩余延迟时间。返回值为正数表示还未到期,负数或零表示已经到期。

支持排序:
Delayed 接口继承了 Comparable 接口,因此实现了该接口的对象可以根据其延迟时间进行比较和排序。
这使得 DelayQueue 能够始终将延迟时间最短(即最早到期)的元素放在队列头部

Delayed 接口的常用实现:
主要看 getDelaycompareTo方法。

// 实现 Delayed 接口
class DelayedDog implements Delayed {
    private String name;        // 狗的名字
    private long delayTime;     // 延迟时间,以毫秒为单位
    private long expire;        // 到期时间点,以毫秒为单位

    // 构造方法,传入狗的名字和延迟时间(毫秒)
    public DelayedDog(String name, long delayTimeInMillis) {
        this.name = name;
        this.delayTime = delayTimeInMillis;
        // 计算到期时间点,当前时间加上延迟时间
        this.expire = System.currentTimeMillis() + delayTime;
    }

    // 获取剩余延迟时间,以给定时间单位表示
    @Override
    public long getDelay(TimeUnit unit) {
        // 计算剩余时间,单位为毫秒
        long remainingTime = expire - System.currentTimeMillis();
        // 将剩余时间转换为指定单位
        return unit.convert(remainingTime, TimeUnit.MILLISECONDS);
    }

    // 比较两个 DelayedDog 对象的顺序
    @Override
    public int compareTo(Delayed other) {
        // 强制转换为 DelayedDog 类型
        DelayedDog otherDog = (DelayedDog) other;
        // 比较两个对象的到期时间点,返回比较结果
        return Long.compare(this.expire, otherDog.expire);
    }
}

6、DelayQueue的put方法

put 方法用于将一个元素插入队列中。
put 调用了 offer 方法,offer方法实现了实际的插入逻辑。

// put 方法:将元素插入队列
public void put(E e) {
   offer(e);
}

// offer 方法:实现实际的插入逻辑
public boolean offer(E e) {
   // 获取锁以确保线程安全
   final ReentrantLock lock = this.lock;
   lock.lock();
   try {
       // 将元素 e 插入优先级队列 q
       q.offer(e);

       // 如果新插入的元素是队列的头部元素
       if (q.peek() == e) {
           // 设置 leader 为 null,表示没有线程在等待获取队列头部元素
           leader = null;

           // 通知等待的线程有新元素可用
           available.signal();
       }

       // 返回 true 表示元素成功插入
       return true;
   } finally {
       // 释放锁
       lock.unlock();
   }
} 

总结下:

  • ①、获取锁:首先获取 lock 对象,并锁定它以确保对队列的并发访问是安全的。
  • ②、插入元素:将元素 e 插入到优先级队列 q 中。PriorityQueue 会根据元素的自然顺序或提供的比较器进行排序。
  • ③、检查并更新当前等待获取队列头部元素的线程,如果新插入的元素 e 是队列的头部元素(即延迟时间最短),将 leader 设置为 null 并调用 available.signal() 方法,通知其他等待的线程有新的元素可用。
  • ④、返回结果
  • ⑤、释放锁:在 finally 块中释放锁,以确保无论插入操作是否成功,锁都会被正确释放,避免死锁的发生。

7、DelayQueue的take方法

take方法用于获取并移除队列的头部元素,如果没有元素达到过期时间则等待。
该方法是阻塞式的。

/**
 * 获取并移除队列的头部元素,如果没有元素达到过期时间则等待。
 *
 * @return 队列的头部元素
 * @throws InterruptedException 如果在等待期间被中断
 */
public E take() throws InterruptedException {
    // 获取用于并发控制的可中断锁
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        for (;;) { // 无限循环,直到成功取出元素
            // 获取队列头部元素,但不移除
            E first = q.peek();
            if (first == null) {
                // 如果队列为空,等待元素变得可用
                available.await();
            } else {
                // 获取头部元素的延迟时间(纳秒)
                long delay = first.getDelay(NANOSECONDS);
                if (delay <= 0) {
                    // 如果延迟时间小于等于0,立即取出头部元素并返回
                    return q.poll();
                }
                first = null; // 在等待期间不保留对头部元素的引用
                if (leader != null) {
                    // 如果当前有其他线程在等待获取队列头部元素,当前线程等待
                    available.await();
                } else {
                    // 当前线程成为“领导者”,负责等待队列头部元素的到期
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
                        // 当前线程等待指定的纳秒时间
                        available.awaitNanos(delay);
                    } finally {
                        // 如果当前线程仍是“领导者”,则重置领导者
                        if (leader == thisThread) {
                            leader = null;
                        }
                    }
                }
            }
        }
    } finally {
        // 如果没有领导者且队列不为空,通知其他等待的线程
        if (leader == null && q.peek() != null) {
            available.signal();
        }
        // 释放锁
        lock.unlock();
    }
}

总结下:

  • ①、获取锁:获取锁以确保线程安全。lockInterruptibly 方法允许当前线程在等待锁的过程中被中断。
  • ②、循环:使用无限循环,直到成功取出一个元素为止。
  • ③、获取队列头部元素:获取队列头部元素但不移除它。如果队列为空,peek 方法返回 null。
  • ④、检查头部元素是否存在:如果队列为空,等待元素变得可用。
  • ⑤、获取头部元素的延迟时间:计算头部元素的延迟时间(单位为纳秒)
  • ⑥、检查延迟时间:如果延迟时间小于等于0,立即移除并返回头部元素。
  • ⑦、处理等待:
    • 在等待期间,不保留对头部元素的引用。(这样做可以减少引用占用的内存,避免内存泄漏)
    • 如果有其他线程在等待获取队列头部元素,当前线程等待。
    • 否则,当前线程成为“领导者”,负责等待头部元素的到期,并等待指定的纳秒时间。
  • ⑧、通知等待线程:如果没有领导者且队列不为空,通知其他等待的线程。
  • ⑨、释放锁: 在 finally 块中释放锁,以确保无论操作是否成功,锁都会被正确释放,避免死锁的发生。

8、DelayQueue的poll方法

该方法是非阻塞的。

/**
 * 从队列中获取并移除头部元素,如果头部元素的延迟时间未到,则返回 null。
 *
 * @return 队列的头部元素,如果不存在或延迟时间未到,则返回 null
 */
public E poll() {
    // 获取用于并发控制的锁
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        // 获取队列头部元素,但不移除
        E first = q.peek();
        // 如果头部元素不存在,或者头部元素的延迟时间大于0,则返回 null
        if (first == null || first.getDelay(NANOSECONDS) > 0) {
            return null;
        } else {
            // 否则,移除并返回头部元素
            return q.poll();
        }
    } finally {
        // 释放锁
        lock.unlock();
    }
}

总结下:

  • ①、获取锁:获取锁以确保线程安全。由于 poll 方法是非阻塞的,所以使用普通的 lock 方法,而不是 lockInterruptibly。
  • ②、尝试获取队列头部元素:获取队列头部元素但不移除它。如果队列为空,peek 方法返回 null。
  • ③、检查头部元素的存在和延迟时间:
    • 如果头部元素不存在(即队列为空),返回 null。
    • 如果头部元素的延迟时间大于0(即还未到期),返回 null。
  • ④、移除并返回头部元素:如果头部元素的延迟时间小于等于0(即已到期),则从队列中移除并返回该元素。
  • ⑤、释放锁:在 finally 块中释放锁,以确保无论操作是否成功,锁都会被正确释放,避免死锁的发生。

补充:带参的poll(long timeout, TimeUnit unit)方法

/**
 * 从队列中获取并移除头部元素,等待指定的时间,如果头部元素的延迟时间未到或超时,则返回 null。
 *
 * @param timeout 等待的最大时间
 * @param unit 时间单位
 * @return 队列的头部元素,如果不存在或延迟时间未到或超时,则返回 null
 * @throws InterruptedException 如果在等待期间被中断
 */
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
    // 将等待时间转换为纳秒
    long nanos = unit.toNanos(timeout);
    // 获取用于并发控制的可中断锁
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        for (;;) { // 无限循环,直到成功取出元素或超时
            // 获取队列头部元素,但不移除
            E first = q.peek();
            if (first == null) {
                // 如果队列为空
                if (nanos <= 0) {
                    return null; // 如果已超时,返回 null
                } else {
                    // 否则,等待指定的纳秒时间
                    nanos = available.awaitNanos(nanos);
                }
            } else {
                // 获取头部元素的延迟时间(纳秒)
                long delay = first.getDelay(NANOSECONDS);
                if (delay <= 0) {
                    return q.poll(); // 如果延迟时间小于等于0,移除并返回头部元素
                }
                if (nanos <= 0) {
                    return null; // 如果已超时,返回 null
                }
                first = null; // 在等待期间不保留对头部元素的引用
                if (nanos < delay || leader != null) {
                    // 如果剩余等待时间小于头部元素的延迟时间或已有领导线程,继续等待
                    nanos = available.awaitNanos(nanos);
                } else {
                    // 当前线程成为“领导者”,负责等待队列头部元素的到期
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
                        long timeLeft = available.awaitNanos(delay); // 等待延迟时间到期
                        nanos -= delay - timeLeft; // 更新剩余等待时间
                    } finally {
                        if (leader == thisThread) {
                            leader = null; // 解除领导者身份
                        }
                    }
                }
            }
        }
    } finally {
        // 如果没有领导者且队列不为空,通知其他等待的线程
        if (leader == null && q.peek() != null) {
            available.signal();
        }
        // 释放锁
        lock.unlock();
    }
}

总结下poll(long timeout, TimeUnit unit)

  • 将等待时间转换为纳秒
  • 获取锁并支持中断
  • 无限循环等待元素可用或超时
  • 尝试获取队列头部元素
  • 检查队列是否为空
  • 获取头部元素的延迟时间
  • 检查延迟时间和剩余等待时间
  • 等待元素可用或超时
  • 通知其他等待线程
  • 释放锁

9、DelayQueue的takepoll方法区别

  • ①、阻塞行为不同:

take 方法:这是一个阻塞方法。如果队列为空或头部元素的延迟时间未到,调用线程会等待直到可以获取到一个元素。
take 方法会一直等待,直到满足条件或线程被中断。

poll 方法:这是一个非阻塞方法。如果队列为空或头部元素的延迟时间未到,它会立即返回 null,而不会等待。

  • ②、返回结果:
    take 方法:总是返回一个元素或在等待期间被中断。如果调用线程被中断,会抛出 InterruptedException。
    poll 方法:立即返回队列头部的元素或 null,如果没有元素可用。

  • ③、使用场景:
    take 方法:适用于消费者线程必须获取元素才能继续工作的情况,特别是消费者-生产者模式中的消费者。
    poll 方法:适用于不希望阻塞线程的情况,可以定期尝试获取元素,但如果没有元素可用,可以继续执行其他任务。

10、DelayQueue的take方法的原理

  • 线程同步:通过 ReentrantLock 实现线程同步,确保多线程环境下的安全访问。
  • 条件变量:通过 Condition 实现线程等待和通知机制,保证线程在没有可用元素时进入等待状态,并在有新元素时被唤醒。
  • 优先级队列:基于优先级队列 (PriorityQueue),确保每次获取的都是延迟时间最短且已到期的元素。

至于 private final Condition available = lock.newCondition(); 本质上是获取Condition 的实例 ConditionObject,ConditionObject是如何实现线程的等待唤醒机制的这里不再讨论了,后续会在并发相关的文章中详细分析相关内容。

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

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

相关文章

微信小程序-人脸核身解决方案

微信小程序-人脸核身解决方案 名词解释 由于不同公司对于 人脸识别的用词不一致&#xff0c;微信小程序背靠腾讯&#xff0c;因此以下的名词主要采集于腾讯云的解释 人脸识别&#xff1a; 主要关注人脸的检测、分析、比对等技术层面&#xff0c;侧重于识别个体身份的技术实现。…

Centos7源码方式安装sqle及开发相关

官方文档-源码安装 操作系统&#xff1a;centos:7.9,everything (DVD版应该也可以) (在ubuntu22.04装了两天之后乖乖开了一个新Centos7虚拟机) 镜像&#xff1a;清华大学开源软件镜像站 centos/7.9.2009 安装git sudo yum update -y sudo yum install -y git git --version安…

使用 Google Gemini 和 SwiftUI 构建 AI 图像识别应用程序

在本教程中,我们将演示如何使用 Google Gemini API 进行图像识别。这个简单的应用程序允许用户从他们的照片库中选择一张图片,并使用 Gemini 描述照片的内容。 在继续本教程之前,请访问Google AI Studio并创建您自己的 API 密钥(如果您还没有这样做)。 在 Xcode 项目中添…

考试系统开源意义

在当今信息科技高速发展的时代&#xff0c;考试系统的代码开源化成为了一个备受关注的话题。开源代码意味着代码的可访问性、可修改性和可分享性&#xff0c;为教育机构和开发者们带来了前所未有的便利和机会。本文将深入探讨考试系统代码开源的背景、意义、优势以及实际应用&a…

Java——IO流(一)-(7/8):字节流-FileOutputStream、字节流完成文件拷贝

目录 文件字节输出流&#xff1a;写字节出去 构造器及常用方法 实例演示 案例&#xff1a;文件复制 过程分析 复制照片 复制文件 文件字节输出流&#xff1a;写字节出去 FileOutputStream&#xff08;文件字节输出流&#xff09; 作用&#xff1a;以内存为基准&#x…

1999年-2022年 商品零售价格指数、城市商品零售价格指数数据

商品零售价格指数&#xff08;RPI&#xff09;是一个关键的经济指标&#xff0c;用于衡量一段时间内商品零售价格的平均变动情况。以下是对商品零售价格指数的详细介绍&#xff1a; 数据简介 定义&#xff1a;商品零售价格指数反映的是与上一年度相比&#xff0c;零售价格的变…

如何利用AI工具高效写作?

利用AI工具进行高效写作已经成为许多人的选择&#xff0c;因为它们能够帮助用户节省时间、提高效率&#xff0c;并在一定程度上保证写作质量。下面小编就和大家分享的一些具体的步骤和建议&#xff0c;帮助大家更好地利用AI工具进行写作。 1.选择合适的AI写作工具 根据自己的写…

【前端】Vue项目和微信小程序生成二维码和条形码

前言&#xff1a;哈喽&#xff0c;大家好&#xff0c;我是前端菜鸟的自我修养&#xff01;今天给大家分享Vue项目和微信小程序如何生成二维码和条形码&#xff0c;介绍了JsBarcode、wxbarcode等插件&#xff0c;并提供具体代码帮助大家深入理解&#xff0c;彻底掌握&#xff01…

图书馆借阅表

DDL 用户表 (Users) 图书表 (Books) 图书类别表 (BookCategories) 图书与类别关联表 (BookCategoryRelations) 借阅记录表 (BorrowRecords) 供应商表 (Suppliers) 采购记录表 (PurchaseRecords) CREATE TABLE Users (user_id INT PRIMARY KEY AUTO_INCREMENT,username …

解决宝塔linux面板 - 404 Not Found(Nginx)方法

宝塔Linux面板后台登录提示404 Not Found Nginx如何解决&#xff1f;码笔记&#xff1a;这是因为BT面板丢失了安全登录入口&#xff0c;如下图&#xff1a; 宝塔404 Not Found nginx 解决方法&#xff1a; 1、先SSH远程服务器 2、然后执行命令 bt 14 重新获取宝塔面板URL地址安…

使用Hugging Face获取BERT预训练模型

【图书推荐】《从零开始大模型开发与微调&#xff1a;基于PyTorch与ChatGLM》_《从零开始大模型开发与微调:基于pytorch与chatglm》-CSDN博客 BERT是一个预训练模型&#xff0c;其基本架构和存档都有相应的服务公司提供下载服务&#xff0c;而Hugging Face是一家目前专门免费提…

cJSON源码解析之add_item_to_object函数

文章目录 前言add_item_to_object函数是干什么的add_item_to_object代码解析函数实现函数原理解析开头的代码constant_key参数的作用最后的if判断 add_item_to_array函数 总结 前言 在我们的日常编程中&#xff0c;JSON已经成为了一种非常常见的数据交换格式。在C语言中&#…

【Android】android studio简单实现图书馆借阅管理系统

希望文章能给到你启发和灵感&#xff5e; 点赞收藏关注 支持一下吧&#xff5e; 阅读指南 序幕一、基础环境说明1.1 硬件环境1.2 软件环境 二、整体设计2.1 数据库逻辑处理&#xff1a;2.2 登录/注册模块2.3 功能界面初始化&#xff1a;2.4 图书管理模块2.5 图书租借服务2.6 读…

[物联网专题] - 螺钉式接线端子的选择和辨识

工业设备上大量使用各式各样的端子来连接外部设备和电缆电线&#xff0c;其中用得最多的就是标准的螺钉式端子&#xff0c;其外形如下&#xff1a; 标准端子一般是2位&#xff08;2个接线端子&#xff09;&#xff0c;端子与端子之间可以级联&#xff0c;组成任意数量的位数。…

vue项目无后台版本打包上传到服务器

打包项目 也可以在文件目录下npm run build 生成dist文件夹 将dist文件夹里的所有文件拷贝到站点的根目录&#xff0c;这里使用宝塔面板进行操作 前提你得先创建站点&#xff0c;域名绑定等操作

项目菜单配置

stores/index.js import {createStore } from "vuex"; //计算高度 let height window.innerHeight;//计算分辨率 let width window.innerWidth;let activeIndex localStorage.getItem("activeIndex"); if (activeIndex null || activeIndex "&q…

制图工具(14)导出图层字段属性信息表

在制图工具&#xff08;13&#xff09;地理数据库初始化工具中我们提到&#xff0c;有一个参数为&#xff1a;“输入Excel表”&#xff0c;并要求表格中的图层字段属性项需要按工具的帮助文档中的示例进行组织… 如下图&#xff1a; 此外&#xff0c;总有那个一个特别的需求&am…

【单片机毕业设计选题24028】-基于STM32的大棚温湿度采集系统

系统功能: 系统分为手动和自动模式&#xff0c;上电默认为自动模式&#xff0c;自动模式下系统根据采集到的传感器值 自动控制&#xff0c;温度过低后自动开启加热&#xff0c;湿度过高后自动开启通风&#xff0c;光照过低后自动开启补 光&#xff0c;水位过低后自动开启水泵…

Android 界面库 (二) 之 Data binding 详细介绍

1. 简介 回顾我们在前面文章《Android 界面库 (一) 之 View binding 简单使用》中学习的 View Binding&#xff0c;它旨在简化 View 与代码之间的绑定过程。它会在编译时期为每个 XML 布局文件生成相应的绑定类(Binding class)&#xff0c;该类里包含了布局文件每个有 ID 的 Vi…

L59---101.对称二叉树(广搜)---Java版

1.题目描述 2.思路和知识点 &#xff08;1)根节点为空&#xff1a; 如果根节点为空&#xff0c;树是对称的。 (2)递归检查&#xff1a; isMirror 方法递归检查两个子树是否是镜像对称的。 (3)辅助函数 isMirror&#xff1a; 1)如果两个节点都为空&#xff0c;它们是镜像对称的…