万字详解,Java实现低配版线程池

文章目录

  • 1.什么是线程池
  • 2.线程池的优势
  • 3.原理
  • 4.代码编写
    • 4.1 阻塞队列
    • 4.2 ThreadPool线程池
    • 4.3 Worker工作线程
    • 4.4 代码测试
  • 5. 拒绝策略
    • 5.1 抽象Reject接口
    • 5.2 BlockingQueue新增tryPut方法
    • 5.3 修改ThreadPool的execute方法
    • 5.4 ThreadPool线程池构造函数修改
    • 5.5 拒绝策略实现
      • 1. 丢弃策略
      • 2. 移除最老元素
      • 3. 死等
      • 4. 抛出异常
    • 5.6 代码测试
  • 6.全部代码

1.什么是线程池

线程池,通过创建了一定数量的线程并将其维护在一个池(即容器)中。当有新的任务提交时,线程池会从池子中分配一个空闲线程来执行任务,而不是每次都新建线程。执行完任务后,线程不会被销毁,而是返回到线程池中等待下一个任务。

2.线程池的优势

  1. 避免线程的重复创建与销毁:对于需要执行的任务,如果每个任务都需要创建一个线程,线程执行完任务后销毁,那么会极大的造成资源的浪费。一方面任务数量可能会很庞大,创建与之匹配的线程会对内存造成严重消耗;另一方面,创建完的线程只工作一次,资本家看了落泪,md血亏啊
  2. 降低资源消耗:创建的线程反复利用,避免了创建与销毁带来的开销
  3. 提高工作的准备时间:被提交的任务可以迅速被线程池中存储的线程执行,无需重新创建

3.原理

线程池中存在以下核心组件

  • 线程池容器(存储工作线程)
  • 任务队列(存储需要执行的任务)

下述代码中,线程池使用HashSet存储;任务队列,使用的是这篇文章实现的BlockingQueue阻塞队列

另外,单纯的Thread线程能够存储的信息太少,因此我们创建Worker对象,extents Thread来包装Thread

下图是线程池的工作流程
在这里插入图片描述
大体来说,线程池执行逻辑分为三大步骤

  1. 如果current thread number < coreSize,创建核心线程执行任务

    tip:

    • current thread number在源码中,是有一个AtomicInteger变量ctl表示。ctl是核心线程池状态控制器,它被分为两个组成部分。其中,高三位表示runStatus,线程池状态;低三位表示workCount,工作线程数量。
    • 选择一个变量ctl同时存储runStatus和workCount,可以通过一次CAS操作实现原子赋值,而不用两次。
  2. 如果核心线程创建失败,或者核心线程数量过多,则将任务存储在阻塞队列中:在这一步中,存在非常多的细节。
    2.1. 如果当前线程池不处于RUNNING状态,尝试创建救急线程运行,不执行入队操作
    2.2. 如果入队失败,同样创建救急线程
    2.3. 如果线程池处于运行状态,且入队成功。进行double-check,重新检查线程池状态ctl
    2.4. 如果此时线程池不处于RUNNIG状态,移除刚入队的任务,并执行reject策略
    2.5. 如果线程池依然处于RUNNIG状态,且工作线程为0,创建救急线程,执行任务
  3. 如果上述步骤均失败,创建救济线程,如果依然失败,执行reject策略

4.代码编写

4.1 阻塞队列

实现请看BlockingQueue阻塞队列,本文不再赘述

4.2 ThreadPool线程池

/**
 * 线程池
 */
@Slf4j
public class ThreadPool {
    // 核心线程数
    private int coreSize;
    // 阻塞队列
    private BlockingQueue<Runnable> workQueue;
    // 队列容量
    private int capacity;
    // 工作线程
    private final HashSet<Worker> workers = new HashSet<>();

	// todo: Worker(详见下一部分)
	private final class Worker extents Thread { /*...*/ }
    
    public ThreadPool(int coreSize, int capacity) {
        this.coreSize = coreSize;
        this.capacity = capacity;
        this.workQueue = new BlockingQueue<>(capacity);
    }

    /**
     * 执行task任务. 如果当前线程数量 < coreSize, 创建线程执行
     * 否则加入阻塞队列. 如果阻塞队列已满, 执行当前拒绝策略
     * @param task 需要执行任务
     */
    public void execute(Runnable task) {
        if (task == null)
            throw new NullPointerException("task is null");
        synchronized (workers) {
            if (workers.size() < coreSize) {
                // 创建线程执行(我们倾向于创建新线程来执行任务, 而非已创建线程)
                log.info("创建worker");
                Worker worker = new Worker(task);
                workers.add(worker);
                worker.start(); // 千万别写成调用run方法, 否则主线程会阻塞(run不会开启线程)
            }else {
                log.info("添加阻塞队列");
                // 添加阻塞队列
                workQueue.put(task);
            }
        }
    }
}

上述代码实现简易版线程池。

  • workQueue:阻塞队列,用于存储待执行的任务
  • coreSize:核心线程数量
  • capacity:阻塞队列大小
  • workers:工作线程的存储容器(线程池),用HashSet实现。请注意,HashSet是线程不安全的,因此在对HashSet操作时,记得加锁保证不会出现并发问题

本节对execute执行逻辑进行一定的简化,暂时不考虑拒绝策略(后续介绍)。

  • 如果当前线程数量 < coreSize,创建核心线程并执行任务
  • 否则添加阻塞队列

tip: 如果任务数量超过阻塞队列容量,那么依据阻塞队列的性质,后续的所有线程都会阻塞,等待容量减少。

4.3 Worker工作线程

我们使用包装过后的线程对象。且Worker是ThreadPool的内部类

private final class Worker extends Thread {
    // 执行的任务
    private Runnable task;

    Worker(Runnable task) {
        this.task = task;
    }

    /**
     * 执行task任务, 如果task为null, 则从workQueue工作队列中获取任务
     * 如果工作队列中不存在等待执行的任务, 终止当前Worker工作线程
     */
    @Override
    public void run() {
        while (task != null || (task = workQueue.take()) != null) {
            try {
                log.info("运行任务");
                task.run();
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                task = null;
            }
        }
        // 移除当前工作线程
        synchronized (workers) {
            workers.remove(this);
        }
    }
}

为了简化代码编写,本文只存在核心线程。核心线程的工作是监视阻塞队列,获取待执行的任务并执行

run方法中,while循环的条件有二

  • task != null: worker线程创建时,会分配第一个待执行的任务。如果待执行的任务不为null,则执行任务
  • task = workQueue.take():worker线程持续监视workQueue阻塞队列中的任务,如果存在任务,获取并执行

tip: workQueue.take()是一个阻塞的方法,没有时间的限制。也就是说,哪怕workQueue为空,该方法也会死等下去

4.4 代码测试

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class Test {
    public static void main(String[] args) throws InterruptedException {
        ThreadPool threadPool = new ThreadPool(2, 5);
        for (int i = 0; i < 10; i++) {
            int j = i;
            // 任务创建时间为2s, 任务消费时间显著低于任务创建时间.
            // 因此本模型是个典型的快生产, 慢消费的模型
            threadPool.execute(() -> {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.info(String.valueOf(j));
            });
        }
    }
}

控制台输出

21:07:34.189 [main] INFO com.fgbg.juc.ThreadPool - 创建worker
21:07:34.202 [main] INFO com.fgbg.juc.ThreadPool - 创建worker
21:07:34.202 [main] INFO com.fgbg.juc.ThreadPool - 添加阻塞队列
21:07:34.205 [main] INFO com.fgbg.juc.ThreadPool - 添加阻塞队列
21:07:34.205 [main] INFO com.fgbg.juc.ThreadPool - 添加阻塞队列
21:07:34.205 [main] INFO com.fgbg.juc.ThreadPool - 添加阻塞队列
21:07:34.205 [main] INFO com.fgbg.juc.ThreadPool - 添加阻塞队列
21:07:34.205 [main] INFO com.fgbg.juc.ThreadPool - 添加阻塞队列
21:07:34.203 [Thread-0] INFO com.fgbg.juc.ThreadPool - 运行任务
21:07:34.209 [Thread-1] INFO com.fgbg.juc.ThreadPool - 运行任务
21:07:36.223 [Thread-0] INFO com.fgbg.juc.Test - 0
21:07:36.223 [main] INFO com.fgbg.juc.ThreadPool - 添加阻塞队列
21:07:36.223 [Thread-0] INFO com.fgbg.juc.ThreadPool - 运行任务
21:07:36.239 [Thread-1] INFO com.fgbg.juc.Test - 1
21:07:36.240 [Thread-1] INFO com.fgbg.juc.ThreadPool - 运行任务
21:07:36.240 [main] INFO com.fgbg.juc.ThreadPool - 添加阻塞队列
21:07:38.239 [Thread-0] INFO com.fgbg.juc.Test - 2
21:07:38.239 [Thread-0] INFO com.fgbg.juc.ThreadPool - 运行任务
21:07:38.256 [Thread-1] INFO com.fgbg.juc.Test - 3
21:07:38.256 [Thread-1] INFO com.fgbg.juc.ThreadPool - 运行任务
21:07:40.250 [Thread-0] INFO com.fgbg.juc.Test - 4
21:07:40.250 [Thread-0] INFO com.fgbg.juc.ThreadPool - 运行任务
21:07:40.265 [Thread-1] INFO com.fgbg.juc.Test - 5
21:07:40.266 [Thread-1] INFO com.fgbg.juc.ThreadPool - 运行任务
21:07:42.252 [Thread-0] INFO com.fgbg.juc.Test - 6
21:07:42.252 [Thread-0] INFO com.fgbg.juc.ThreadPool - 运行任务
21:07:42.268 [Thread-1] INFO com.fgbg.juc.Test - 7
21:07:42.268 [Thread-1] INFO com.fgbg.juc.ThreadPool - 运行任务
21:07:44.260 [Thread-0] INFO com.fgbg.juc.Test - 8
21:07:44.275 [Thread-1] INFO com.fgbg.juc.Test - 9

线程池核心线程数为2,因此一开始迅速创建2个worker线程。但因为阻塞队列容量为5,且每个线程工作需要2s,耗时远远小于任务产出的速度,因此队列被迅速沾满

当提交第8个任务时,主线程进入阻塞状态,无法继续提交任务(2个任务正在执行 + 5个任务添加阻塞队列 + 1个任务刚要入队,就阻塞了)

当第一个任务被执行完成,Thread-0 Worker执行阻塞队列中的其他任务。此时存在多余位置,之前被阻塞主线程成功提交任务,并继续循环

后续的流程大体一致,故不在做多余分析。

5. 拒绝策略

所谓拒绝策略,就是提供给调用方一个选择。如果调用方提交了过量的任务,多余的任务作何种处理。

由上方代码分析可知,我们一开始对于过量的任务,处理方案就是死等。但这种方案无法满足其他特定的需求,比如某个场景对执行速度有要求,等待一段时间后阻塞队列依然无法处理额外的任务,那么主线程就要抛弃该任务。死等是处理的方式之一,但存在不少的局限性,我们需要更多的处理方式。

对于不同的处理方式,我们可以选择将代码写死在ThreadPool中,但这样太不灵活,对于不同的场景,我们需要添加大量if else。因此我们可以采用策略模式,将拒绝的行为抽象成一个接口,创建ThreadPool时,由调用方传递接口。这样我们就可以在不改变ThreadPool内部代码的同时,改变ThreadPool面对超量任务的拒绝行为

5.1 抽象Reject接口

@FunctionalInterface
public interface RejectPolicy {
    // 执行拒绝策略
    void reject(Runnable task, BlockingQueue<Runnable> workQueue);
}

5.2 BlockingQueue新增tryPut方法

tryPut方法,尝试将元素立刻添加到阻塞队列中,不支持阻塞等待

// 尝试立即添加元素
public boolean tryPut(T task) {
    lock.lock();
    try {
        if (deque.size() == capacity) return false;
        deque.addLast(task);
        return true;
    } finally {
        lock.unlock();
    }
}

5.3 修改ThreadPool的execute方法

execute执行task入队操作时,如果入队失败(阻塞队列已满),则调用reject执行拒绝策略

    public void execute(Runnable task) {
        if (task == null)
            throw new NullPointerException("task is null");
        synchronized (workers) {
            if (workers.size() < coreSize) {
                // 创建线程执行(我们倾向于创建新线程来执行任务, 而非已创建线程)
                log.info("创建worker");
                Worker worker = new Worker(task);
                workers.add(worker);
                worker.start(); // 千万别写成调用run方法, 否则主线程会阻塞(run不会开启线程)
            }else {
                log.info("添加阻塞队列");
                /*----------------modify below-------------------*/
                // 添加阻塞队列
                // workQueue.put(task);
                // 添加失败
                if ( !workQueue.tryPut(task)) {
                    // 执行拒绝策略
                    rejectPolicy.reject(task, workQueue);
                }
            }
        }
    }

5.4 ThreadPool线程池构造函数修改

    // 拒绝策略
    private RejectPolicy rejectPolicy;
    
    public ThreadPool(int coreSize, int capacity, RejectPolicy rejectPolicy) {
        this(coreSize, capacity);
        this.rejectPolicy = rejectPolicy;
    }

5.5 拒绝策略实现

因为RejectPolicy接口有@FunctionalInterface,支持lambda表达式,因此编写的时候可以简写

1. 丢弃策略

(task, workQueue) -> {}

2. 移除最老元素

(task, workQueue) -> { workQueue.poll(); }

tip: 笔者自定义的BlockingQueue没有实现poll方法,各位读者如果感兴趣,可以自行实现。需要注意的是,记得加锁保证线程安全

3. 死等

(task, workQueue) -> { workQueue.put(task); }

4. 抛出异常

(task, workQueue) -> { new RuntimeException("workQueue is full"); }

5.6 代码测试

@Slf4j
public class Test3 {
    public static void main(String[] args) throws InterruptedException {
        ThreadPool threadPool = new ThreadPool(2, 5, (task, workQueue) -> {
            log.info("任务丢弃");
        });
        for (int i = 0; i < 10; i++) {
            int j = i;
            threadPool.execute(() -> {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.info(String.valueOf(j));
            });
        }
    }
}

上述代码选择的拒绝策略是丢弃

控制台输出

21:46:37.621 [main] INFO com.fgbg.juc.ThreadPool - 创建worker
21:46:37.630 [main] INFO com.fgbg.juc.ThreadPool - 创建worker
21:46:37.631 [main] INFO com.fgbg.juc.ThreadPool - 添加阻塞队列
21:46:37.631 [main] INFO com.fgbg.juc.ThreadPool - 添加阻塞队列
21:46:37.631 [Thread-0] INFO com.fgbg.juc.ThreadPool - 运行任务
21:46:37.631 [main] INFO com.fgbg.juc.ThreadPool - 添加阻塞队列
21:46:37.631 [main] INFO com.fgbg.juc.ThreadPool - 添加阻塞队列
21:46:37.631 [main] INFO com.fgbg.juc.ThreadPool - 添加阻塞队列
21:46:37.631 [main] INFO com.fgbg.juc.ThreadPool - 添加阻塞队列
21:46:37.631 [main] INFO com.fgbg.juc.Test3 - 任务丢弃
21:46:37.631 [main] INFO com.fgbg.juc.ThreadPool - 添加阻塞队列
21:46:37.631 [main] INFO com.fgbg.juc.Test3 - 任务丢弃
21:46:37.632 [main] INFO com.fgbg.juc.ThreadPool - 添加阻塞队列
21:46:37.632 [main] INFO com.fgbg.juc.Test3 - 任务丢弃
21:46:37.633 [Thread-1] INFO com.fgbg.juc.ThreadPool - 运行任务
21:46:39.636 [Thread-1] INFO com.fgbg.juc.Test3 - 1
21:46:39.636 [Thread-0] INFO com.fgbg.juc.Test3 - 0
21:46:39.636 [Thread-0] INFO com.fgbg.juc.ThreadPool - 运行任务
21:46:39.636 [Thread-1] INFO com.fgbg.juc.ThreadPool - 运行任务
21:46:41.644 [Thread-0] INFO com.fgbg.juc.Test3 - 3
21:46:41.645 [Thread-0] INFO com.fgbg.juc.ThreadPool - 运行任务
21:46:41.644 [Thread-1] INFO com.fgbg.juc.Test3 - 2
21:46:41.645 [Thread-1] INFO com.fgbg.juc.ThreadPool - 运行任务
21:46:43.651 [Thread-1] INFO com.fgbg.juc.Test3 - 5
21:46:43.651 [Thread-0] INFO com.fgbg.juc.Test3 - 4
21:46:43.651 [Thread-1] INFO com.fgbg.juc.ThreadPool - 运行任务
21:46:45.657 [Thread-1] INFO com.fgbg.juc.Test3 - 6

由日志可知,第8,9,10号任务被丢弃。任务对应的输出为7,8,9。观察输出的数字,发现最大值为6。因此确认了7~10号任务全部被拒绝,测试成功

6.全部代码

BlockingQueue


import java.util.ArrayDeque;
import java.util.Deque;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

// 消息队列(阻塞队列)
class BlockingQueue<T> {
    // 队列
    private Deque<T> deque = new ArrayDeque<>();
    // 容量
    private int capacity;
    // 锁
    private final ReentrantLock lock = new ReentrantLock();
    // 消费者等待条件
    private Condition consumerWaitSet = lock.newCondition();
    // 生产者等待条件
    private Condition producerWaitSet = lock.newCondition();

    public BlockingQueue(int capacity) {
        this.capacity = capacity;
    }

    // 添加元素
    public void put(T element) {
        lock.lock();
        try {
            // 队列已满
            while (deque.size() == capacity) {
                try {
                    // 阻塞等待
                    producerWaitSet.await();
                } catch (InterruptedException e) {
                }
            }
            // 添加元素
            deque.addLast(element);
            // 唤醒其它线程
            consumerWaitSet.signal();
        } finally {
            lock.unlock();
        }
    }

    // 获取元素
    public T take() {
        lock.lock();
        try {
            // 判空
            while (deque.size() == 0) {
                try {
                    // 阻塞等待
                    consumerWaitSet.await();
                } catch (InterruptedException e) {
                }
            }
            // 获取元素
            T res = deque.pollFirst();
            producerWaitSet.signal();
            return res;
        } finally {
            lock.unlock();
        }
    }

    // 尝试立即添加元素
    public boolean tryPut(T task) {
        lock.lock();
        try {
            if (deque.size() == capacity) return false;
            deque.addLast(task);
            return true;
        } finally {
            lock.unlock();
        }
    }
}

RejectPolicy

@FunctionalInterface
public interface RejectPolicy {
    // 执行拒绝策略
    void reject(Runnable task, BlockingQueue<Runnable> workQueue);
}

ThreadPool

import lombok.extern.slf4j.Slf4j;

import java.util.HashSet;

/**
 * 线程池
 */
@Slf4j
public class ThreadPool {
    // 核心线程数
    private int coreSize;
    // 阻塞队列
    private BlockingQueue<Runnable> workQueue;
    // 队列容量
    private int capacity;
    // 工作线程
    private final HashSet<Worker> workers = new HashSet<>();
    // 拒绝策略
    private RejectPolicy rejectPolicy;

    private final class Worker extends Thread {
        // 执行的任务
        private Runnable task;

        Worker(Runnable task) {
            this.task = task;
        }

        /**
         * 执行task任务, 如果task为null, 则从workQueue工作队列中获取任务
         * 如果工作队列中不存在等待执行的任务, 终止当前Worker工作线程
         */
        @Override
        public void run() {
            while (task != null || (task = workQueue.take()) != null) {
                try {
                    log.info("运行任务");
                    task.run();
                } catch (Exception e) {
                    e.printStackTrace();
                } finally {
                    task = null;
                }
            }
            // 移除当前工作线程
            synchronized (workers) {
                workers.remove(this);
            }
        }
    }

    public ThreadPool(int coreSize, int capacity) {
        this.coreSize = coreSize;
        this.capacity = capacity;
        this.workQueue = new BlockingQueue<>(capacity);
    }

    public ThreadPool(int coreSize, int capacity, RejectPolicy rejectPolicy) {
        this(coreSize, capacity);
        this.rejectPolicy = rejectPolicy;
    }

    /**
     * 执行task任务. 如果当前线程数量 < coreSize, 创建线程执行
     * 否则加入阻塞队列. 如果阻塞队列已满, 执行当前拒绝策略
     * @param task 需要执行任务
     */
    public void execute(Runnable task) {
        if (task == null)
            throw new NullPointerException("task is null");
        synchronized (workers) {
            if (workers.size() < coreSize) {
                // 创建线程执行(我们倾向于创建新线程来执行任务, 而非已创建线程)
                log.info("创建worker");
                Worker worker = new Worker(task);
                workers.add(worker);
                worker.start(); // 千万别写成调用run方法, 否则主线程会阻塞(run不会开启线程)
            }else {
                log.info("添加阻塞队列");
                // 添加阻塞队列
                // workQueue.put(task);
                // 添加失败
                if ( !workQueue.tryPut(task)) {
                    // 执行拒绝策略
                    rejectPolicy.reject(task, workQueue);
                }
            }
        }
    }
}

Test

import lombok.extern.slf4j.Slf4j;

@Slf4j
public class Test {
    public static void main(String[] args) throws InterruptedException {
        ThreadPool threadPool = new ThreadPool(2, 5);
        for (int i = 0; i < 10; i++) {
            int j = i;
            threadPool.execute(() -> {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.info(String.valueOf(j));
            });
        }
    }
}

Test3

@Slf4j
public class Test3 {
    public static void main(String[] args) throws InterruptedException {
        ThreadPool threadPool = new ThreadPool(2, 5, (task, workQueue) -> {
            log.info("任务丢弃");
        });
        for (int i = 0; i < 10; i++) {
            int j = i;
            threadPool.execute(() -> {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                log.info(String.valueOf(j));
            });
        }
    }
}

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Win11右键菜单改回Win10

按“win键 X”&#xff0c;在弹出的快捷菜单中&#xff0c;选择“Windows终端&#xff08;管理员&#xff09;”&#xff1a; 弹出黑窗口&#xff0c;并把下面的语句复制进去&#xff1a; reg add "HKCU\Software\Classes\CLSID\{86ca1aa0-34aa-4e8b-a509-50c905bae2a2}…

五、循环神经网络语言模型(RNN)

1 循环神经网络基础知识 循环核&#xff08;Recurrent Cell&#xff09;定义&#xff1a; 指在时刻 t 时的神经网络单元&#xff0c;用来处理当前时刻的输入和上一时刻的隐藏状态&#xff0c;并生成当前时刻的输出和下一时刻的隐藏状态。记忆体&#xff08;Memory&#xff09;定…

Numpy 数组转换为 Pandas DataFrame

参考&#xff1a;Convert Numpy Array to Pandas DataFrame Numpy 介绍 Numpy是Python中一个非常强大的科学计算库&#xff0c;它提供了许多高效的数组操作方法。Pandas是另一个重要的数据处理库&#xff0c;它基于Numpy&#xff0c;并提供了更高级别的数据分析和处理工具。在…

windows下编译boost1.84.0库

boost系列文章目录 文章目录 boost系列文章目录前言一、boost编译二、boost使用三 、参考 前言 Boost简介 官方网址 Boost提供免费的同行评审的可移植C源代码库。 我们强调与C标准库配合良好的库。Boost库旨在广泛使用&#xff0c;并可在广泛的应用程序中使用。Boost许可证鼓…

#微信小程序(布局、渲染层基础知识)

1.IDE&#xff1a;微信开发者工具 2.实验&#xff1a; 3.记录: &#xff08;1&#xff09;view&#xff08;类似于div&#xff09; &#xff08;2&#xff09;块级元素不占满一行且水平均分布局flex,justify(space-around) &#xff08;3&#xff09;滚动<scroll view sc…

【Web - 框架 - Vue】随笔 - Vue的简单使用(01) - 快速上手

【Web - 框架 - Vue】随笔 - Vue的简单使用(01) - 快速上手 Vue模板代码 代码 <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><title>模板</title> </head> <body> <div></di…

2024 年系统集成项目管理师全套资料

2024年11月系统集成项目管理工程师全套视频、历年真题及解析、教材、模拟题、重点笔记等资料 1、2024年全套视频持续更新中&#xff0c;2023年11月全套基础知识精讲视频、2023年5月全套基础知识精讲视频。 2、系统集成项目管理工程师2009-2023年11月历年真题及解析&#xff08…

Mock在接口测试中的实际应用

关于Mock测试 01、含义和目的 1、 什么是mock测试&#xff1f; Mock 测试就是在测试过程中&#xff0c;对于某些不容易构造&#xff08;如 HttpServletRequest 必须在Servlet 容器中才能构造出来&#xff09;或者不容易获取的比较复杂的对象&#xff08;如 JDBC 中的ResultSe…

yolov8训练CDLA数据文版版面分析

一.数据集介绍 CDlA数据集介绍&#xff1a;CDLA CDLA是一个中文文档版面分析数据集&#xff0c;面向中文文献类&#xff08;论文&#xff09;场景。包含以下10个label&#xff1a; 数据量&#xff1a; 共包含5000张训练集和1000张验证集&#xff0c;分别在train和val目录下。每…

今天分享一个好看的输入法皮肤相信每个人心里住着一个少女心我们美化一下她吧

标题&#xff1a; 白日梦皮肤上线&#xff0c;百度输入法助你开启梦幻之旅&#xff01; 正文&#xff1a; 大家好呀&#xff01;今天我来给大家安利一款超级梦幻的百度输入法皮肤——“白日梦”系列&#xff01; 这款皮肤的设计灵感来源于我们内心深处的白日梦&#xff0c;充…

技术实践|数据迁移中GBK转UTF8字符集问题分析

导语&#xff1a;在国产化创新的大背景下&#xff0c;数据库迁移项目逐渐增多&#xff0c;在数据库迁移过程中&#xff0c;源数据库和目标数据库字符集有时会不同&#xff0c;这时如何进行字符集转换则成为了一个重要的问题&#xff0c;同时在转换过程中还需要确保数据的完整性…

武汉灰京文化:游戏推广的领军者

在当今飞速发展的游戏行业中&#xff0c;游戏推广成为了每个游戏开发商和发行商必然要面对的挑战。如何能够将游戏信息传播给更广泛的受众群体&#xff0c;提升游戏的知名度和用户参与度&#xff0c;成为了每个游戏从业者需要思考的问题。而武汉灰京文化作为游戏推广领域的领军…

如何减少AI中的偏见问题:八种方法避免AI偏见渗入模型

克服与避免 AI 偏见的八大方法 AI 中的算法偏见是一个普遍存在的问题&#xff0c;它虽然不可能完全消除&#xff0c;但却可以通过科学的方法积极地防止这种偏见。我们将在本文中围绕如何应对AI中的偏见问题展开深入的讨论。 您可能会回想起新闻中报道的一些存在偏见的算法示例…

springboot248校园资产管理

校园资产管理 摘 要 现代经济快节奏发展以及不断完善升级的信息化技术&#xff0c;让传统数据信息的管理升级为软件存储&#xff0c;归纳&#xff0c;集中处理数据信息的管理方式。本校园资产管理就是在这样的大环境下诞生&#xff0c;其可以帮助管理者在短时间内处理完毕庞大…

试用Claude3

1 简介 好消息是&#xff0c;2024 年 3 月 4 日发布了 Claude3&#xff0c;据传比 GPT-4 更好&#xff0c;snooet 版本可以免费试用&#xff0c;坏消息是我们这儿不能用。 在官网注册时&#xff0c;需要选择国家并使用手机接收短信验证码。而在选项中没有中国这个选项。即使成…

【Maven】Maven 基础教程(五): jar 包冲突问题

《Maven 基础教程》系列&#xff0c;包含以下 5 篇文章&#xff1a; Maven 基础教程&#xff08;一&#xff09;&#xff1a;基础介绍、开发环境配置Maven 基础教程&#xff08;二&#xff09;&#xff1a;Maven 的使用Maven 基础教程&#xff08;三&#xff09;&#xff1a;b…

贪心 Leetcode 763 划分字母区间

划分字母区间 Leetcode 763 学习记录自代码随想录 给你一个字符串 s 。我们要把这个字符串划分为尽可能多的片段&#xff0c;同一字母最多出现在一个片段中。 注意&#xff0c;划分结果需要满足&#xff1a;将所有划分结果按顺序连接&#xff0c;得到的字符串仍然是 s 。 返…

JAVA语言基础 JAVA入门

注释 单行注释&#xff1a;用双斜线 // 表示 多行注释&#xff1a;用 /*------------------*/ 表示 文档注释&#xff1a;用 /**-----------------*/ 表示 分隔符 常见的分隔符有&#xff1a;分号 ; 花括号 {} 方括号 [ ] 圆括号 () 空格 圆点 . 在 Java 语言中每一条…

LeetCode 刷题 [C++] 第300题.最长递增子序列

题目描述 给你一个整数数组 nums &#xff0c;找到其中最长严格递增子序列的长度。 子序列 是由数组派生而来的序列&#xff0c;删除&#xff08;或不删除&#xff09;数组中的元素而不改变其余元素的顺序。例如&#xff0c;[3,6,2,7] 是数组 [0,3,1,6,2,2,7] 的子序列。 题目…

快递包装展|2024上海国际电商物流包装产业展览会

2024中国(上海)国际电商物流包装产业展览会 2024 China (Shanghai) international e-commerce logistics packaging industry exhibition 时 间&#xff1a;2024年7月24日 —7月26日 地 点&#xff1a;国家会展中心&#xff08;上海市青浦区崧泽大道333号&#xff…