Java 并发编程面试题——BlockingQueue

news2024/10/5 15:34:03

目录

  • 1.什么是阻塞队列 (BlockingQueue)?
  • 2.BlockingQueue 有哪些核心方法?
  • 3.BlockingQueue 有哪些常用的实现类?
    • 3.1.ArrayBlockingQueue
    • 3.2.DelayQueue
    • 3.3.LinkedBlockingQueue
    • 3.4.PriorityBlockingQueue
    • 3.5.SynchronousQueue
  • 4.✨BlockingQueue 的实现原理是什么?
    • 4.1.构造器
    • 4.2.put 操作
    • 4.3.take 操作
  • 5.BlockingQueue 的使用场景有哪些?
    • 5.1.生产者—消费者模式
    • 5.2.线程池中使用阻塞队列
  • 6.✨手动实现一个简单的阻塞队列?

1.什么是阻塞队列 (BlockingQueue)?

BlockingQueuejava.util.concurrent 包下重要的数据结构,与普通的队列不同,BlockingQueue 提供了线程安全的队列访问方式,该包下很多高级同步类的实现都是基于 BlockingQueue 实现的。BlockingQueue ⼀般用于生产者——消费者模式,生产者是往队列里添加元素的线程, 消费者是从队列里取出元素的线程。BlockingQueue 就是存放元素的容器

2.BlockingQueue 有哪些核心方法?

(1)BlockingQueue 是一个接口,继承自 Queue,所以其实现类也可以作为 Queue 的实现来使用,而 Queue 又继承自 Collection 接口。

在这里插入图片描述

(2)BlockingQueue 中的核心方法如下:

方法 \ 处理方式抛出异常返回特殊值一直阻塞超时退出
插入add(e)offer(e)put(e)offer(e, time, unit)
移除remove()poll()take()poll(time, unit)
检查element()peek()--
  • 抛出异常:如果试图的操作无法立即执行,抛出异常。当阻塞队列满时候,再往队列里插入元素,会抛出 IllegalStateException(“Queue full”) 异常。当队列为空时,从队列里获取元素时会抛出 NoSuchElementException 异常。
  • 返回特殊值:如果试图的操作无法立即执行,返回⼀个特殊值,通常是 true / false。
  • ⼀直阻塞:如果试图的操作无法立即执行,则⼀直阻塞或者响应中断。
  • 超时退出:如果试图的操作无法立即执行,该⽅法调⽤将会发生阻塞,直到能够执行,但等待时间不会超过给定值。返回⼀个特定值以告知该操作是否成功,通常是 true / false。

注意:
① 不能往阻塞队列中插入 null,否则会抛出空指针异常。
② 可以访问阻塞队列中的任意元素,调用 remove(o) 可以将队列之中的特定对象移除,但并不高效,尽量避免使用。

(3)BlockingQueue 的源码如下:

package java.util.concurrent;

import java.util.Collection;
import java.util.Queue;

public interface BlockingQueue<E> extends Queue<E> {
    
    boolean add(E e);

    boolean offer(E e);

    void put(E e) throws InterruptedException;

    boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException;

    E take() throws InterruptedException;
  
    E poll(long timeout, TimeUnit unit)
        throws InterruptedException;

    int remainingCapacity();

    boolean remove(Object o);

    public boolean contains(Object o);

    int drainTo(Collection<? super E> c);

    int drainTo(Collection<? super E> c, int maxElements);
}

3.BlockingQueue 有哪些常用的实现类?

下面主要介绍 5 个常用的 BlockingQueue 实现类。

在这里插入图片描述

3.1.ArrayBlockingQueue

(1)ArrayBlockingQueue 是由数组结构组成的有界阻塞队列。内部结构是数组,故具有数组的特性。其源码中的构造函数如下:

public class ArrayBlockingQueue<E> extends AbstractQueue<E>
        implements BlockingQueue<E>, java.io.Serializable {
	
	//...

	public ArrayBlockingQueue(int capacity) {
        this(capacity, false);
    }
	
	public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        //...
    }
	
	public ArrayBlockingQueue(int capacity, boolean fair,
                              Collection<? extends E> c) {
        this(capacity, fair);
        //...
    }
}

(2)ArrayBlockingQueue 一旦创建,容量不能改变。其并发控制采用可重入锁 ReentrantLock,不管是插入操作还是读取操作,都需要获取到锁才能进行操作。当队列容量满时,尝试将元素放入队列将导致操作阻塞;尝试从一个空队列中取一个元素也会同样阻塞。

(3)ArrayBlockingQueue 默认情况下不能保证线程访问队列的公平性,所谓公平性是指严格按照线程等待的绝对时间顺序,即最先等待的线程能够最先访问到 ArrayBlockingQueue。而非公平性则是指访问 ArrayBlockingQueue 的顺序不是遵守严格的时间顺序,有可能存在当 ArrayBlockingQueue 可以被访问时,长时间阻塞的线程依然无法访问到 ArrayBlockingQueue 的情况。如果保证公平性,那么通常会降低吞吐量。如果需要获得公平性的 ArrayBlockingQueue,可采用如下代码:

ArrayBlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(10, true);

3.2.DelayQueue

(1)DelayQueue 中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue 的应用场景主要是处理具有延迟需求的任务调度,比如定时任务、缓存过期等等。它提供了一种方便的方式来实现元素的延迟处理。。源码中的构造方法如下所示:

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

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

(2)DelayQueue 的特点如下:

  • 元素按照延迟时间的顺序进行排序,延迟时间越短的元素排在队列的前面。
  • 元素只有在指定的延迟时间到达后才可以从队列中取出。
  • 如果队列中没有到达延迟时间的元素,那么从队列中取元素的操作将会被阻塞,直到有元素到达延迟时间。
  • DelayQueue 是线程安全的,多个线程可以安全地操作同一个 DelayQueue 实例。

(3)DelayQueue 的底层实现原理是基于 PriorityQueue(优先级队列)和 ReentrantLock(可重入锁)实现的。

  • DelayQueue 使用 PriorityQueue 作为其内部数据结构,这是一个基于堆的优先级队列,用于存储元素并按照其延迟时间进行排序。在 PriorityQueue 中,延迟时间越短的元素排在队列的前面。
  • 当元素被插入到 DelayQueue 中时,会根据元素的延迟时间进行排序,并被放置在对应的位置。取出元素时,会检查队列头部的元素是否已经到达了延迟时间,如果还未到达延迟时间,则会阻塞等待。当元素的延迟时间到达后,该元素可以被取出。
  • 为了保证线程安全性和可并发性,DelayQueue 使用了 ReentrantLock 进行同步,确保多个线程可以安全地操作 DelayQueue。
  • 另外,DelayQueue 中的元素需要实现 Delayed 接口,这个接口定义了两个方法:getDelay(TimeUnit unit)compareTo(Delayed o)。前者用于获取当前元素的剩余延迟时间,后者用于比较两个元素的延迟时间大小。这样,DelayQueue就能根据元素的延迟时间进行有序排列。

总结起来,DelayQueue 的底层实现原理是基于 PriorityQueue 和 ReentrantLock。PriorityQueue 用于按照延迟时间对元素进行排序,ReentrantLock 用于保证线程安全性。通过这两者的组合,DelayQueue 可以有效地实现具有延迟需求的任务调度功能。

(4)使用 DelayQueue 的示例如下:

class DelayedElement implements Delayed {
    private String element;
    private long delayTime;

    public DelayedElement(String element, long delayTime) {
        this.element = element;
        this.delayTime = System.currentTimeMillis() + delayTime;
    }

    @Override
    public long getDelay(TimeUnit unit) {
        long remainingTime = delayTime - System.currentTimeMillis();
        return unit.convert(remainingTime, TimeUnit.MILLISECONDS);
    }

    @Override
    public int compareTo(Delayed other) {
        long diff = getDelay(TimeUnit.MILLISECONDS) - other.getDelay(TimeUnit.MILLISECONDS);
        return Long.compare(diff, 0);
    }

    public String getElement() {
        return element;
    }
}

class DelayQueueExample {
    public static void main(String[] args) throws InterruptedException {
        DelayQueue<DelayedElement> delayQueue = new DelayQueue<>();

        //添加延时元素到队列中
        delayQueue.add(new DelayedElement("Element 1", 2000));
        delayQueue.add(new DelayedElement("Element 2", 500));
        delayQueue.add(new DelayedElement("Element 3", 3000));

        //从队列中取出延时元素
        while (!delayQueue.isEmpty()) {
            DelayedElement element = delayQueue.take();
            System.out.println("取出元素:" + element.getElement());
        }
    }
}

3.3.LinkedBlockingQueue

(1)LinkedBlockingQueue 底层基于单向链表实现的阻塞队列,可以当做无界队列也可以当做有界队列来使用,同样满足 FIFO 的特性,与 ArrayBlockingQueue 相比起来具有更高的吞吐量,为了防止 LinkedBlockingQueue 容量迅速增,损耗大量内存。通常在创建 LinkedBlockingQueue 对象时,会指定其大小,如果未指定,容量等于 Integer.MAX_VALUE

(2)源码中的构造方法如下所示:

public class LinkedBlockingDeque<E>
    extends AbstractQueue<E>
    implements BlockingDeque<E>, java.io.Serializable {
	
	//...

	public LinkedBlockingDeque() {
        this(Integer.MAX_VALUE);
    }
	
	public LinkedBlockingDeque(int capacity) {
        if (capacity <= 0) throw new IllegalArgumentException();
        this.capacity = capacity;
    }

	public LinkedBlockingDeque(Collection<? extends E> c) {
        this(Integer.MAX_VALUE);
        //...
    }
}

3.4.PriorityBlockingQueue

(1)PriorityBlockingQueue 是一个支持优先级的无界阻塞队列。默认情况下元素采用自然顺序进行排序,也可以通过自定义类实现 compareTo() 方法来指定元素排序规则,或者初始化时通过构造器参数 Comparator 来指定排序规则。

(2)PriorityBlockingQueue 并发控制采用的是可重入锁 ReentrantLock,队列为无界队列(ArrayBlockingQueue 是有界队列,LinkedBlockingQueue 也可以通过在构造函数中传入 capacity 指定队列最大的容量,但是 PriorityBlockingQueue 只能指定初始的队列大小,后面插入元素的时候,如果空间不够的话会自动扩容)。简单地说,它就是 PriorityQueue 的线程安全版本。不可以插入 null 值,同时,插入队列的对象必须是可比较大小的 (comparable),否则报 ClassCastException 异常。它的插入操作 put 方法不会 block,因为它是无界队列(take 方法在队列为空的时候会阻塞)。

(3)源码中的构造方法如下所示:

public class PriorityBlockingQueue<E> extends AbstractQueue<E>
    implements BlockingQueue<E>, java.io.Serializable {
	
	//...

	public PriorityBlockingQueue() {
        this(DEFAULT_INITIAL_CAPACITY, null);
    }
	
	public PriorityBlockingQueue(int initialCapacity) {
        this(initialCapacity, null);
    }	
	
	public PriorityBlockingQueue(int initialCapacity,
                                 Comparator<? super E> comparator) {
        //...
    }
	
	public PriorityBlockingQueue(Collection<? extends E> c) {
        //...
    }
}

3.5.SynchronousQueue

SynchronousQueue 这个队列比较特殊,没有任何内部容量,是一种无缓冲的等待队列,类似于无中介的直接交易。并且每个 put 必须等待⼀个 take,反之亦然。 需要区别容量为 1 的 ArrayBlockingQueue、LinkedBlockingQueue。以下方法的返回值,可以帮助理解这个队列:

方法返回值
iterator()永远返回 null
peek()永远返回 null
put()往队列里放进去⼀个元素以后就⼀直等待,直到有其他线程将该元素取走
offer()往队列里放⼀个元素后立即返回,如果碰巧该元素被另⼀个线程取走了,那么该方法返回 true,认为 offer 成功;否则返回 false
take()取出队列中的元素,若取不到则一直等待
poll()只有到碰巧另外⼀个线程正在往队列中 offer 元素或者 put 元素时,该方法才会取到元素;否则立即返回 null
isEmpty()永远返回 true
remove() & removeAll()永远返回 false

4.✨BlockingQueue 的实现原理是什么?

阻塞队列的原理很简单,利⽤了 Lock 锁的多条件 (Condition) 阻塞控制。下面对 ArrayBlockingQueue JDK 1.8 的源码进行分析。

4.1.构造器

首先是构造器,除了初始化队列的大小和是否是公平锁之外,还对同⼀个锁 (lock) 初始化了两个监视器,分别是 notEmptynotFull。这两个监视器的作用目前可以简单理解为标记分组:

  • 当该线程是 put 操作时,给他加上监视器 notFull,标记这个线程是⼀个生产者
  • 当线程是 take 操作时,给他加上监视器 notEmpty,标记这个线程是⼀个消费者
public class ArrayBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable {
	//数据元素数组
	final Object[] items;
	//下⼀个待取出元素索引
	int takeIndex;
	//下⼀个待添加元素索引
	int putIndex;
	//元素个数
	int count;
	//内部锁
	final ReentrantLock lock;
	//消费者监视器
	private final Condition notEmpty;
	//⽣产者监视器
	private final Condition notFull; 
	public ArrayBlockingQueue(int capacity, boolean fair) {
		 //..省略其他代码
		 lock = new ReentrantLock(fair);
		 notEmpty = lock.newCondition();
		 notFull = lock.newCondition();
	}

	//...
}

4.2.put 操作

put 的流程如下:

  • 所有执行 put 操作的线程竞争 lock 锁,拿到了 lock 锁的线程进入下⼀步,没有拿到 lock 锁的线程自旋竞争锁
  • 判断阻塞队列是否满了:
    • 如果满了,则调用 notFull.await() 方法阻塞这个线程,并标记为 notFull(生产者)线程,同时释放 lock 锁,等待被消费者线程唤醒。
    • 如果没有满,则调用 enqueue 方法将元素 put 进阻塞队列。注意这⼀步的线程还有⼀种情况是第⼆步中阻塞的线程被唤醒且又拿到了lock 锁的线程。
  • 释放 lock 锁,并唤醒⼀个标记为 notEmpty(消费者)的线程。
public void put(E e) throws InterruptedException {
    checkNotNull(e);
    final ReentrantLock lock = this.lock;
    //⾃旋拿锁
    lock.lockInterruptibly();
    try {
        //判断队列是否满了
        while (count == items.length)
            //如果满了,阻塞该线程,并标记为 notFull 线程,等待 notEmpty 的唤醒,唤醒之后继续执⾏ while 循环
            notFull.await();
        //如果没有满,则进⼊队列
        enqueue(e);
    } finally {
        lock.unlock();
    }
}

private void enqueue(E x) {
    // assert lock.getHoldCount() == 1;
    // assert items[putIndex] == null;
    final Object[] items = this.items;
    items[putIndex] = x;
    if (++putIndex == items.length)
        putIndex = 0;
    count++;
    //唤醒⼀个等待的线程
    notEmpty.signal();
}

4.3.take 操作

take 操作和 put 操作的流程是类似的,总结⼀下 take 操作的流程:

  • 所有执行 take 操作的线程竞争 lock 锁,拿到了 lock 锁的线程进入下⼀步,没有拿到 lock 锁的线程自旋竞争锁。
  • 判断阻塞队列是否为空:
    • 如果是空,则调用 notEmpty.await 方法阻塞这个线程,并标记为 notEmpty(消费者)线程,同时释放 lock 锁,等待被生产者线程唤醒。
    • 如果没有空,则调用 dequeue 方法。注意这⼀步的线程还有⼀种情况是第⼆步中阻塞的线程被唤醒且又拿到了 lock 锁的线程。
  • 释放 lock 锁,并唤醒⼀个标记为 notFull(生产者)的线程。
public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        while (count == 0)
            notEmpty.await();
        return dequeue();
    } finally {
        lock.unlock();
    }
}

private E dequeue() {
    // assert lock.getHoldCount() == 1;
    // assert items[takeIndex] != null;
    final Object[] items = this.items;
    @SuppressWarnings("unchecked")
    E x = (E) items[takeIndex];
    items[takeIndex] = null;
    if (++takeIndex == items.length)
        takeIndex = 0;
    count--;
    if (itrs != null)
        itrs.elementDequeued();
    notFull.signal();
    return x;
}

相关知识点:
Java 并发编程面试题——Condition 接口

5.BlockingQueue 的使用场景有哪些?

5.1.生产者—消费者模式

public class Test {
    int queueSize = 10;
    BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(queueSize);
    
    public static void main(String[] args) {
        Test test = new Test();
        Producer producer = test.new Producer();
        Consumer consumer = test.new Consumer();
        producer.start();
        consumer.start();
    }
    
    class Consumer extends Thread {
        @Override
        public void run() {
            consume();
        }
        
        private void consume() {
            while (true) {
                try {
                    queue.take();
                    System.out.println("从队列取走⼀个元素,队列剩余 " + queue.size() + " 个元素");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    class Producer extends Thread {
        @Override
        public void run() {
            produce();
        }
        
        private void produce() {
            while (true) {
                try {
                    queue.put(1);
                    System.out.println("向队列取中插入⼀个元素,队列剩余空间:" + (queue.size()));
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

上述代码的一个结果片段如下:

从队列取⾛⼀个元素,队列剩余 0 个元素 
从队列取⾛⼀个元素,队列剩余 0 个元素 
向队列取中插⼊⼀个元素,队列剩余空间:9 
向队列取中插⼊⼀个元素,队列剩余空间:9 
向队列取中插⼊⼀个元素,队列剩余空间:9 
向队列取中插⼊⼀个元素,队列剩余空间:8 
向队列取中插⼊⼀个元素,队列剩余空间:7 
向队列取中插⼊⼀个元素,队列剩余空间:6 
向队列取中插⼊⼀个元素,队列剩余空间:5 
向队列取中插⼊⼀个元素,队列剩余空间:4 
向队列取中插⼊⼀个元素,队列剩余空间:3 
向队列取中插⼊⼀个元素,队列剩余空间:2 
向队列取中插⼊⼀个元素,队列剩余空间:1 
向队列取中插⼊⼀个元素,队列剩余空间:0 
从队列取⾛⼀个元素,队列剩余 1 个元素 
从队列取⾛⼀个元素,队列剩余 9 个元素

注意,这个例子中的输出结果看起来可能有问题,比如有几行在插入⼀个元素之后,队列的剩余空间不变。这是由于 System.out.println 语句没有锁。考虑到这样的情况:线程 1 在执行完 put/take 操作后立即失去 CPU 时间片,然后切换到线程 2 执行 put/take 操作,执行完毕后回到线程 1 的 System.out.println 语句并输出,发现这个时候阻塞队列的 size 已经被线程 2 改变了,所以这个时候输出的 size 并不是当时线程 1 执行完 put/take 操作之后阻塞队列的 size,但可以确保的是 size 不会超过 10 个。实际上使用阻塞队列是没有问题的。

5.2.线程池中使用阻塞队列

(1)ThreadPoolExecutor.java 中的一个构造函数源码如下:

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
    this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
         Executors.defaultThreadFactory(), defaultHandler);
}

(2)Java 中的线程池就是使用阻塞队列实现的,我们在了解阻塞队列之后,无论是使用 Exectors 类中已经提供的线程池,还是自己通过 ThreadPoolExecutor 实现线程池,都会比较方便。

6.✨手动实现一个简单的阻塞队列?

(1)以下是一个使用 Java 手写的基本阻塞队列的示例:

import java.util.LinkedList;
import java.util.Queue;

public class BlockingQueue<T> {

    private Queue<T> queue;
    private int capacity;

    public BlockingQueue(int capacity) {
        this.queue = new LinkedList<>();
        this.capacity = capacity;
    }

    public synchronized void enqueue(T item) throws InterruptedException {
        while (queue.size() == capacity) {
            wait(); // 如果队列已满,等待直到有空间
        }
        queue.add(item);
        notifyAll(); // 通知其他线程队列中有新元素
    }
    
    public synchronized T dequeue() throws InterruptedException {
        while (queue.isEmpty()) {
            wait(); // 如果队列为空,等待直到有元素可出队
        }
        T item = queue.poll();
        notifyAll(); // 通知其他线程队列中有空间
        return item;
    }
}

上述代码定义了一个泛型的阻塞队列类 BlockingQueue,基于一个内部的 LinkedList 实现队列。

  • 构造函数接受一个容量参数用于限制队列的大小。
  • enqueue 方法用于将元素添加到队尾。如果队列已满,则当前线程进入等待状态,直到有空间可用。添加元素后,使用 notifyAll() 方法通知其他线程有新元素可用。
  • dequeue 方法用于从队头获取并移除一个元素。如果队列为空,则当前线程进入等待状态,直到有元素可出队。获取元素后,使用notifyAll()方法通知其他线程有空间可用。

(2)请注意,这只是一个简单的示例,可能还需要进行更多的安全性和异常处理的优化。此外,阻塞队列的实现可以有多种方式,这只是其中的一种。在实际使用时,你还需要根据具体需求进行适当的调整和扩展。

(3)InterruptedException 的说明:

  • InterruptedException 是 Java 中的一个异常类,用于表示在线程处于阻塞状态时被中断的情况。当一个线程调用了处于阻塞状态的方法(如 Thread.sleep()Object.wait()BlockingQueue.take() 等)时,如果该线程被其他线程调用了 interrupt() 方法中断,那么该阻塞方法将抛出 InterruptedException 异常。
  • InterruptedException 是一个受检查的异常,意味着在使用阻塞方法时必须显式地处理或传递这个异常。当抛出 InterruptedException 时,线程的中断状态将被清除,即 Thread.interrupted() 方法会返回 false。
  • 主要情况下,我们对 InterruptedException 的处理方式通常有两种:
    • 向上抛出异常:在方法的签名中声明 throws InterruptedException,将异常传递给调用者处理。这要求调用者必须在调用时处理或继续传递该异常。
    • 恢复中断状态:在 catch 块中捕获 InterruptedException,然后根据需要重新中断线程,通常是通过调用 Thread.currentThread().interrupt() 方法。这样做是为了保持线程的中断状态,以便其他线程能够检测到中断并做出响应。

下面是一个示例,展示了如何处理 InterruptedException:

public void myMethod() {
    try {
        // 执行可能会抛出 InterruptedException 的操作
        Thread.sleep(1000);
    } catch (InterruptedException e) {
        // 恢复线程的中断状态
        Thread.currentThread().interrupt();

        // 可以进行其他的处理
        System.out.println("Caught InterruptedException: " + e.getMessage());
    }
}

在上述示例中,myMethod 方法使用 Thread.sleep() 方法模拟一个可能被中断的操作。当线程在执行 sleep 方法时被中断时,将抛出 InterruptedException。在 catch 块中,我们恢复线程的中断状态,并进行其他适当的处理,如打印异常信息。通过正确处理 InterruptedException,我们可以更好地响应线程的中断请求,并采取适当的措施来维护线程的中断状态。

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

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

相关文章

【C++】构造函数和析构函数第二部分(拷贝构造函数)--- 2023.9.28

目录 什么是拷贝构造函数&#xff1f;编译器默认的拷贝构造函数构造函数的分类及调用结束语 什么是拷贝构造函数&#xff1f; 用一句话来描述为拷贝构造即 “用一个已知的对象去初始化另一个对象” 具体怎么使用我们直接看代码&#xff0c;代码如下&#xff1a; class Maker…

什么是DOM和DOM操作

什么是DOM&#xff1f; DOM&#xff08;文档对象模型&#xff09;:HTML文档的结构化表示。允许JavaScript访问HTML元素和样式来操作它们。&#xff08;更改文本&#xff0c;HTML属性甚至CSS样式&#xff09; 树结构由HTML加载后自动生成 DOM树结构 这个是一个很简单的HTML代…

Redis与分布式-主从复制

接上文 常用中间件-OAuth2 1.主从复制 启动两个redis服务器。 修改第一个服务器地址 修改第二个redis 然后分别启动 redis-server.exe redis.windows.conf) 查看当前服务器的主从状态&#xff0c;打开客户端&#xff1a;输入info replication命令来查看当前的主从状态&am…

数据结构基础9:排序全家桶

排序全家桶&#xff1a; 一&#xff1a;插入排序&#xff1a;1.简单插入排序&#xff1a;2.希尔排序&#xff1a; 二&#xff1a;选择排序&#xff1a;1.简单选择排序&#xff1a;2.堆排序&#xff08;空间复杂度为O(1)&#xff09;: 三&#xff1a;快速排序&#xff1b;方法一…

共同见证丨酷雷曼武汉运营中心成立2周年

酷雷曼武汉运营中心2周年 全国合作商齐贺武汉公司2周年庆 2021年 作为酷雷曼辐射全国版图的又一重要据点 酷雷曼武汉运营中心 在“中国光谷”正式成立 沉浸式参观酷雷曼武汉公司 2年时间 尽管历经诸多客观因素的挑战 但后浪扬帆&#xff0c;依然交出了不斐的成绩 解决…

用AI写文章被百家号封禁

我是卢松松&#xff0c;点点上面的头像&#xff0c;欢迎关注我哦&#xff01; 千万不要用AI创作&#xff0c;尤其是原文照搬!不要用ai,不要用&#xff0c;不要用!重要的事情说三遍。 近日ID名为“爸爸在家赚钱”用AI写了4-5篇文章投稿在百家号&#xff0c;随后百度就把他帐号…

【Bond与你白话IaC之Terraform for Docker篇】 攻城狮如何向女友解释IaC呢?

前言&#xff1a; 最近有机会与朋友聊到IaC&#xff08;Infra as code&#xff09;说到是否有比较好的切入点进行学习。 客观地说&#xff0c;看到XaX或XasX结构的的名词&#xff0c;让人立刻会与最前沿的云技术联系起来&#xff0c;但实际上其背后的思想仍然来自于传统系统的痛…

python web编程一:token、session、cookie、密码加解密

1 认证 1 传统的session-cookie机制 HTTP协议是无状态协议&#xff0c;为了解决它产生了cookie和session技术。 浏览器发起第一次请求到服务器&#xff0c;服务器发现浏览器没有提供session id&#xff0c;就认为这是第一次请求&#xff0c;会返回一个新的session id给浏览器…

Flask扩展:简化开发的利器以及26个日常高效开发的第三方模块(库/插件)清单和特点总结

目录 寻找扩展 使用扩展 创建扩展 26个常用的Flask扩展模块 总结 原文&#xff1a;Flask扩展&#xff1a;简化开发的利器以及26个日常高效开发的第三方模块&#xff08;库/插件&#xff09;清单和特点总结 (qq.com) Flask是一个轻量级的Python Web框架&#xff0c;它提供…

14.(开发工具篇github)如何在Github配置ssh key

第一步&#xff1a;检查本地主机是否已经存在ssh key 上图表示已存在。跳第三步 第二步&#xff1a;生成ssh key ssh-keygen -t rsa -C "xxxxxx.com"第三步&#xff1a;获取ssh key公钥内容&#xff08;id_rsa.pub&#xff09; cat id_rsa.pub第四步&#xff1a;G…

如何将图片转为ico格式

这里主要是记录一个网站&#xff0c;如果你有更好的办法欢迎留言~ ico简介 ICO&#xff08;Icon&#xff09;是一种用于表示图标的文件格式&#xff0c;常用于Windows操作系统中。ICO格式的图片通常用于表示应用程序、文件夹、网站等的图标。 ICO文件可以包含多个图标&#x…

在 .NET 8 Release Candidate 1 中推出 .NET MAUI:质量

作者&#xff1a;David Ortinau 排版&#xff1a;Alan Wang 今天&#xff0c;我们很高兴地宣布 .NET MAUI 在 .NET 8 Release Candidate 1 中已经可用&#xff0c;该版本带有适用于生产应用程序的正式许可证&#xff0c;因此您可以放心地将此版本用于生产环境。我们在 .NET 8 中…

用代码打造未来教育:在线教育平台开发的奇妙之旅

当我们谈论在线教育平台开发时&#xff0c;我们正在谈论一项颠覆性的技术&#xff0c;它改变了传统教育的面貌。在线教育已经成为21世纪的教育主题&#xff0c;使学习变得更加灵活、便捷和个性化。本文将探讨在线教育平台开发的关键方面&#xff0c;并穿插一些代码示例来帮助您…

wordpress插件-免费的wordpress全套插件

在当今数字化时代&#xff0c;网站和博客已经成为信息传递、观点分享和商业交流的重要平台。在这个背景下&#xff0c;WordPress作为最受欢迎的内容管理系统之一&#xff0c;无疑扮演着至关重要的角色。然而&#xff0c;要保持一个成功的WordPress网站&#xff0c;不仅需要出色…

不要二(牛客)

目录 一、题目 二、代码 一、题目 不要二__牛客网 二、代码 采用贪心算法的思想来做&#xff0c;开始全置为1&#xff0c;1代表放入蛋糕。 从左向右从上到下遍历棋盘开始依此放蛋糕&#xff0c;然后将该块蛋糕上下左右欧几里得距离为2的点全部标记为0&#xff0c;表示该点不…

泛函分析(一)

目录 1.数学基本概念 2.泛函概念和应用 2.1常用知识点 2.2泛函数解决的问题 2.3核函数 3.应用 参考文献 1.数学基本概念 2.泛函概念和应用 2.1常用知识点 算子&#xff1a;无限维空间到无限维空间的变换称为。泛函数&#xff1a;就是函数的函数&#xff0c;即一般函数自…

二、C++项目:仿muduo库实现并发服务器之时间轮的设计

文章目录 一、为什么要设计时间轮&#xff1f;&#xff08;一&#xff09;简单的秒级定时任务实现&#xff1a;&#xff08;二&#xff09;Linux提供给我们的定时器&#xff1a;1.原型2.例子 二、时间轮&#xff08;一&#xff09;思想&#xff08;一&#xff09;代码 一、为什…

基于SpringBoot网上超市的设计与实现【附万字文档(LW)和搭建文档】

主要功能 前台登录&#xff1a; 注册用户&#xff1a;用户名、密码、姓名、联系电话 用户&#xff1a; ①首页、商品信息推荐、商品资讯、查看更多 ②商品信息、商品详情、评论、点我收藏、添加购物车、立即购买 ③个人中心、余额、点我充值、更新信息、我的订单、我的地址、我…

国庆出游,景区该怎么接住这泼天的流量?媒介媒介盒子告诉你

国庆出游&#xff0c;景区该怎么接住这泼天的流量&#xff1f;媒介媒介盒子告诉你 假期倒计时。这一次&#xff0c;几亿人又要大规模出游了&#xff0c;景区最不愁的&#xff0c;就是没有流量。那么景区该怎么接住这泼天的流量呢&#xff1f; 1、利用社交媒体营销。 利用微信…

26591-2011 粮油机械 糙米精选机

声明 本文是学习GB-T 26591-2011 粮油机械 糙米精选机. 而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 1 范围 本标准规定了糙米精选机的有关术语和定义、工作原理、型号及基本参数、技术要求、试验方法、检 验规则、标志、包装、运输和储存要求。 …