Java 并发编程:Java 线程池的介绍与使用

news2024/9/22 23:25:20

大家好,我是栗筝i,这篇文章是我的 “栗筝i 的 Java 技术栈” 专栏的第 024 篇文章,在 “栗筝i 的 Java 技术栈” 这个专栏中我会持续为大家更新 Java 技术相关全套技术栈内容。专栏的主要目标是已经有一定 Java 开发经验,并希望进一步完善自己对整个 Java 技术体系来充实自己的技术栈的同学。与此同时,本专栏的所有文章,也都会准备充足的代码示例和完善的知识点梳理,因此也十分适合零基础的小白和要准备工作面试的同学学习。当然,我也会在必要的时候进行相关技术深度的技术解读,相信即使是拥有多年 Java 开发经验的从业者和大佬们也会有所收获并找到乐趣。

在现代软件开发中,随着计算资源的增多和应用需求的复杂化,如何高效地管理并发任务成为一个关键问题。无论是处理海量数据的后台服务,还是需要实时响应的用户界面应用,都离不开多线程编程。Java 作为一种广泛使用的编程语言,提供了丰富的并发工具,其中线程池是实现高效并发的核心组件之一。

线程池通过重用现有的线程,减少了频繁创建和销毁线程的开销,同时能够有效地控制并发任务的数量,防止系统过载。借助线程池,开发者可以更方便地管理和调度任务,提高系统的响应速度和资源利用率。然而,正确理解和使用线程池并非易事,合理配置线程池参数、选择适当的拒绝策略、避免常见陷阱都是确保系统稳定运行的关键。

本文将深入介绍 Java 线程池的基本概念、常用类型及其适用场景,并结合实际代码示例展示如何在不同场景下合理使用线程池。希望通过本篇文章,读者能对 Java 线程池有一个全面的了解,并能在实际开发中应用自如,从而编写出更高效、更稳定的并发程序。


文章目录

      • 1、Java 线程池介绍
      • 2、Java 线程池的执行流程
        • 2.1、线程池的几个重要参数
        • 2.2、线程池的执行流程
        • 2.3、拒绝策略
          • 2.3.1、AbortPolicy
          • 2.3.2、CallerRunsPolicy
          • 2.3.3、DiscardPolicy
          • 2.3.4、DiscardOldestPolicy
          • 2.3.5、自定义拒绝策略
        • 2.4、线程池状态
      • 3、Java 线程池的使用
        • 3.1、常用的线程池
        • 3.2、Executor 框架
        • 3.3、ThreadPoolExecutor创建线程池
        • 3.4、Executor 框架的继承关系


1、Java 线程池介绍

池化技术现在已经屡见不鲜了,线程池、数据库连接池、Http 连接池等等都是对这个思想的应用。池化技术的思想主要是为了减少每次获取资源的消耗,提高对资源的利用率。

线程池(Thread Pool)是一种基于池化思想管理线程的工具,由于创建和关闭线程需要花费时间,如果为每一个任务都创建一个线程,非常消耗资源。使用线程池可以避免增加创建和销毁线程的资源消耗,提高响应速度,且能重复利用线程。在使用线程池后,创建线程就变成了从线程池中获取空闲线程,关闭线程变成了向线程池归还线程。

线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,使用完毕不需要销毁线程而是放回池中,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。他的主要特点为:线程复用、控制最大并发数、管理线程。

使用线程池的好处:

  • 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度:当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

2、Java 线程池的执行流程

2.1、线程池的几个重要参数

线程池通过复用线程来提高性能并减少线程创建和销毁的开销。

public class ThreadPoolExecutor extends AbstractExecutorService {
  
    // 省略其他方法和实现细节
    ...

     /**
     * 创建一个新的 {@code ThreadPoolExecutor} 实例,使用给定的初始参数。
     *
     * @param corePoolSize    保持在池中的线程数量,即使它们是空闲的,除非设置了 {@code allowCoreThreadTimeOut}
     * @param maximumPoolSize 允许在池中存在的最大线程数量
     * @param keepAliveTime   当线程数量大于核心线程数时,多余的空闲线程在终止之前等待新任务的最长时间
     * @param unit            {@code keepAliveTime} 参数的时间单位
     * @param workQueue       用于在任务执行前保存任务的队列。这个队列只会保存通过 {@code execute} 方法提交的 {@code Runnable} 任务
     * @param threadFactory   当执行器创建新线程时使用的工厂
     * @param handler         当执行由于线程边界和队列容量达到限制而被阻塞时使用的处理程序
     * @throws IllegalArgumentException 如果以下任一情况成立:<br>
     *                                  {@code corePoolSize < 0}<br>
     *                                  {@code keepAliveTime < 0}<br>
     *                                  {@code maximumPoolSize <= 0}<br>
     *                                  {@code maximumPoolSize < corePoolSize}
     * @throws NullPointerException     如果 {@code workQueue} 或 {@code threadFactory} 或 {@code handler} 为 null
     */
    public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler) {
        // 检查传入参数的合法性
        if (corePoolSize < 0 ||
                maximumPoolSize <= 0 ||
                maximumPoolSize < corePoolSize ||
                keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();

        // 初始化参数
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime); // 转换 keepAliveTime 为纳秒
        this.threadFactory = threadFactory;
        this.handler = handler;

        // 创建线程池容器
        String name = Objects.toIdentityString(this);
        this.container = SharedThreadContainer.create(name);
    }
  
    // 省略其他方法和实现细节
    ...
  
}

在配置线程池时,有几个重要参数需要理解和配置:

  1. 核心线程数(corePoolSize):这是线程池中始终保持存活的线程数量。即使这些线程处于空闲状态,线程池也不会销毁它们;核心线程数决定了线程池在接收到任务时初始的并发处理能力;

  2. 最大线程数(maximumPoolSize):这是线程池中允许的最大线程数量。当任务队列已满且核心线程都在忙碌时,线程池会创建新线程直到达到最大线程数。这个参数决定了线程池能够处理的最大并发任务数量;

  3. 空闲线程存活时间(keepAliveTime):当线程池中的线程数超过核心线程数,且这些超出核心线程数的线程空闲时间超过了 keepAliveTime,这些线程会被终止并从池中移除;这个参数有助于在任务负载减小时减少资源消耗;

  4. 时间单位(unit):这是 keepAliveTime 参数的时间单位,如秒(TimeUnit.SECONDS)、毫秒(TimeUnit.MILLISECONDS)等;

  5. 任务队列(workQueue):用于存放等待执行的任务。常见的任务队列有 LinkedBlockingQueueSynchronousQueueArrayBlockingQueue 等;任务队列的选择会影响线程池的行为和性能。例如,SynchronousQueue 不存储任务,而是直接将任务交给工作线程处理;

  6. 线程工厂(threadFactory):用于创建新线程。可以自定义线程工厂来设置线程的名称、优先级等。例如,可以使用 Executors.defaultThreadFactory() 来获得默认的线程工厂;

  7. 拒绝策略(RejectedExecutionHandler):当任务队列已满且线程池中的线程数量已达到最大线程数时,线程池会执行拒绝策略。

    常见的拒绝策略包括:

    • AbortPolicy:抛出 RejectedExecutionException 异常。
    • CallerRunsPolicy:由调用线程执行任务。
    • DiscardPolicy:直接丢弃任务,不抛出异常。
    • DiscardOldestPolicy:丢弃队列中最旧的任务,然后尝试提交新的任务。

配置合理的线程池参数可以有效地管理资源,提高系统的并发处理能力,并确保系统的稳定性和响应速度。在实际应用中,需要根据具体的需求和工作负载进行调优。

2.2、线程池的执行流程

线程池(ThreadPoolExecutor)的执行流程如下:

public class ThreadPoolExecutor extends AbstractExecutorService {
  
    // 省略其他方法和实现细节
    ...
      
		/**
     * 在将来的某个时间执行给定的任务。任务可能在一个新的线程中执行,也可能在一个已有的线程池线程中执行。
     * <p>
     * 如果任务不能被提交执行,无论是因为这个执行器已经关闭还是因为它的容量已经达到了极限,任务都将由当前的
     * {@link RejectedExecutionHandler} 处理。
     *
     * @param command 要执行的任务
     * @throws RejectedExecutionException 在 {@code RejectedExecutionHandler} 的判断下,如果任务不能被接受执行
     * @throws NullPointerException       如果 {@code command} 为 null
     */
    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        /*
         * 分三步进行:
         *
         * 1. 如果运行的线程少于 corePoolSize,尝试启动一个新的线程并将给定的命令作为其第一个任务。
         *    对 addWorker 的调用会原子地检查运行状态和工作线程数,防止在不应添加线程时误报警。
         *
         * 2. 如果任务能够成功入队,那么我们仍然需要再次检查是否应该添加一个线程(因为现有的线程在上次检查后可能已经终止)
         *    或者线程池自进入此方法以来是否已关闭。因此我们重新检查状态,如果必要的话在停止时回滚入队操作,
         *    或者如果没有线程则启动一个新的线程。
         *
         * 3. 如果我们不能将任务入队,那么我们尝试添加一个新的线程。如果失败,我们知道要么是已经关闭,要么是已饱和,
         *    因此拒绝任务。
         */
        // 获取线程池的状态和工作线程数
        int c = ctl.get();
        // 如果工作线程数小于核心线程数
        if (workerCountOf(c) < corePoolSize) {
            // 尝试添加一个新的工作线程来执行任务
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        // 如果线程池在运行且任务成功入队
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            // 如果线程池不再运行且任务被成功移除
            if (!isRunning(recheck) && remove(command))
                // 拒绝任务
                reject(command);
                // 如果没有工作线程
            else if (workerCountOf(recheck) == 0)
                // 添加一个新的工作线程
                addWorker(null, false);
            // 如果不能将任务入队且添加新线程失败
        } else if (!addWorker(command, false))
            // 拒绝任务
            reject(command);
    }
  
    // 省略其他方法和实现细节
    ...
  
}

以下是对这个流程的详细步骤介绍:

  1. 任务提交:通过 execute(Runnable) 方法或 submit(Callable<T>) 方法向线程池提交任务。

  2. 线程处理任务:线程池首先判断核心线程池中的线程数是否已达到 corePoolSize

    • 如果没有达到,创建一个新的线程来处理任务;
    • 如果达到了,将任务放入工作队列 workQueue
  3. 任务队列:

    • 如果工作队列未满,任务被添加到队列中等待执行。
    • 如果工作队列已满,且线程池中的线程数小于 maximumPoolSize,则创建新的线程来处理任务。
    • 如果工作队列已满,且线程池中的线程数已达到 maximumPoolSize,则执行拒绝策略(RejectedExecutionHandler)。
  4. 线程执行任务:

    • 线程从任务队列中取出任务并执行;
    • 当线程完成任务后,会继续从队列中取出下一个任务执行。
  5. 线程存活时间:

    • 当线程池中的线程数超过核心线程数,并且这些多余的线程空闲时间超过 keepAliveTime,则这些线程会被终止并从池中移除。
    • 这样可以避免线程池在任务负载较低时占用过多资源。

图示化线程池执行流程:

  任务提交
     |
  是否可以创建核心线程
   /   \
是      否
/         \
创建核心线程 将任务放入工作队列
                 |
               是否队列已满
                /     \
              否      是
             /          \
        放入队列    是否可以创建新线程
                          /     \
                        是       否
                       /           \
               创建新线程    执行拒绝策略
2.3、拒绝策略

线程池的拒绝策略(RejectedExecutionHandler)用于处理当线程池无法执行新的任务时的情况。主要有四种内置的拒绝策略,分别是:

2.3.1、AbortPolicy

这是默认的拒绝策略。当线程池无法处理新的任务时,它将抛出 RejectedExecutionException。使用场景:当你希望在任务无法被执行时立即得到通知,并采取相应的措施。

示例代码:

RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();
2.3.2、CallerRunsPolicy

当线程池无法处理新的任务时,该策略会让提交任务的线程直接运行这个任务。使用场景:当你希望减缓新任务的提交速度时,该策略可以让调用线程参与执行任务,从而降低任务提交的速率。

示例代码:

RejectedExecutionHandler handler = new ThreadPoolExecutor.CallerRunsPolicy();
2.3.3、DiscardPolicy

当线程池无法处理新的任务时,该策略会直接丢弃被拒绝的任务,不做任何处理也不抛出异常。使用场景:当你可以接受某些任务被静默丢弃且不需要额外的处理时。

示例代码:

RejectedExecutionHandler handler = new ThreadPoolExecutor.DiscardPolicy();
2.3.4、DiscardOldestPolicy

当线程池无法处理新的任务时,该策略会丢弃队列中最旧的任务,然后重新尝试提交被拒绝的任务。使用场景:当你希望优先处理最新的任务,可以接受丢弃一些旧任务时。

示例代码:

RejectedExecutionHandler handler = new ThreadPoolExecutor.DiscardOldestPolicy();
2.3.5、自定义拒绝策略

除了上述四种内置策略外,你还可以实现 RejectedExecutionHandler 接口来创建自定义的拒绝策略。例如:

public class CustomRejectedExecutionHandler implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
        // 自定义拒绝处理逻辑,例如记录日志或将任务放入另一个队列中
        System.out.println("Task " + r.toString() + " rejected from " + executor.toString());
    }
}
2.4、线程池状态

Java 线程池(ThreadPoolExecutor)的状态主要通过一个称为 ctl 的原子变量来表示,这个变量同时包含了线程池的运行状态和线程池中的有效线程数量。ctl 变量是一个 AtomicInteger 类型的变量,其高3位表示线程池的状态,低29位表示线程池中的线程数量。

线程池的状态主要有以下几种:

  1. RUNNING:线程池在正常运行状态,可以接受新的任务,并处理队列中的任务。
  2. SHUTDOWN:调用了 shutdown() 方法后,线程池进入该状态,不再接受新任务,但会继续处理队列中的任务。
  3. STOP:调用了 shutdownNow() 方法后,线程池进入该状态,不再接受新任务,并且会中断正在处理的任务和清空队列中的任务。
  4. TIDYING:所有任务都已终止,工作线程数为零,线程池将要调用 terminated() 方法。
  5. TERMINATED:terminated() 方法已经完成执行,线程池完全终止。

ctl 变量的高3位表示线程池状态,低29位表示工作线程数量。以下是几个常量的定义:

private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

private static final int RUNNING    = -1 << COUNT_BITS;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

通过这些常量的定义,可以看出线程池状态的高 3 位是如何设置的。

线程池在不同状态之间的转换流程如下:

  1. RUNNING -> SHUTDOWN:调用 shutdown() 方法后,线程池状态从 RUNNING 转变为 SHUTDOWN,线程池不再接受新任务,但会继续处理已提交的任务。
  2. (RUNNING or SHUTDOWN) -> STOP:调用 shutdownNow() 方法后,线程池状态从 RUNNING 或 SHUTDOWN 转变为 STOP,线程池不再接受新任务,并中断所有正在执行的任务。
  3. SHUTDOWN -> TIDYING:当所有任务都已完成,线程池状态从 SHUTDOWN 转变为 TIDYING。
  4. STOP -> TIDYING:当所有任务都已中断,线程池状态从 STOP 转变为 TIDYING。
  5. TIDYING -> TERMINATED:terminated() 方法执行完毕后,线程池状态从 TIDYING 转变为 TERMINATED。

以下是线程池状态转换的示例代码:

public void shutdown() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        advanceRunState(SHUTDOWN);
        interruptIdleWorkers();
        onShutdown(); // hook for ScheduledThreadPoolExecutor
    } finally {
        mainLock.unlock();
    }
    tryTerminate();
}

public List<Runnable> shutdownNow() {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        checkShutdownAccess();
        advanceRunState(STOP);
        interruptWorkers();
        taskQueue.clear();
    } finally {
        mainLock.unlock();
    }
    return super.shutdownNow();
}

shutdown() 方法中,通过 advanceRunState(SHUTDOWN) 将状态转变为 SHUTDOWN,在 shutdownNow() 方法中,通过 advanceRunState(STOP) 将状态转变为 STOP


3、Java 线程池的使用

3.1、常用的线程池

在 Java 中,Executors 提供了一些便捷的方法来创建常用的线程池,但在实际开发中,为了避免潜在的问题和更好地控制线程池的行为,通常建议直接使用 ThreadPoolExecutor 来创建线程池。下面是对一些常用线程池的介绍和它们可能存在的问题:

newSingleThreadExecutor:
创建一个单线程的线程池,只有一个线程在工作,所有任务按照提交顺序执行。如果唯一的线程因为异常终止,会有一个新线程替代它继续工作。使用场景:适用于需要确保顺序执行任务的场景。

代码示例:

ExecutorService executor = Executors.newSingleThreadExecutor();

newFixedThreadPool:
描述:创建一个固定大小的线程池。每次提交一个任务就创建一个线程,直到达到线程池的最大大小。线程池大小达到最大值后,将继续保持固定大小,如果某个线程因为异常终止,会补充一个新线程。使用场景:适用于需要限制并发线程数,控制资源使用的场景。

代码示例:

ExecutorService executor = Executors.newFixedThreadPool(10);

newCachedThreadPool
描述:创建一个可缓存的线程池。如果线程池中有空闲线程可以重用,则会重用空闲线程;如果没有空闲线程,则创建新线程。空闲线程会在60秒没有任务执行时被终止并移除。使用场景:适用于执行许多短期异步任务的小程序,或者负载较轻的服务器。

代码示例:

ExecutorService executor = Executors.newCachedThreadPool();

newScheduledThreadPool
描述:创建一个支持定时和周期性任务执行的线程池。可以用于需要定时执行任务的场景。使用场景:适用于需要定时或者周期性执行任务的场景。

代码示例:

ScheduledExecutorService executor = Executors.newScheduledThreadPool(5);

使用 Executors 的弊端:阿里巴巴 Java 开发手册建议不要使用 Executors 创建线程池,而是直接使用 ThreadPoolExecutor,主要是为了避免以下问题:

  • newFixedThreadPoolnewSingleThreadExecutor:问题:使用无界的请求队列 LinkedBlockingQueue,可能会导致请求堆积,耗尽内存,甚至导致 OutOfMemoryError

  • newCachedThreadPoolnewScheduledThreadPool:问题:允许创建的线程数最大值为 Integer.MAX_VALUE,可能会创建大量线程,耗尽系统资源,甚至导致 OutOfMemoryError

为了更好地控制线程池的行为,建议使用 ThreadPoolExecutor 直接创建线程池。这样可以更明确地控制线程池的核心参数,如核心线程数、最大线程数、空闲线程存活时间、任务队列等。

3.2、Executor 框架

Java 中的线程池是通过 Executor 框架实现的,该框架中用到了 ExecutorExecutorsExecutorServiceThreadPoolExecutorCallableFutureFutureTask 这几个类

Executor 框架是 Java5 之后引进的,在 Java 5 之后,通过 Executor 来启动线程比使用 Thread 的 start 方法更好,除了更易管理,效率更好(用线程池实现,节约开销)外,还有关键的一点:有助于避免 this 逃逸问题。

this 逃逸是指在构造函数返回之前其他线程就持有该对象的引用. 调用尚未构造完全的对象的方法可能引发令人疑惑的错误。

Executor 框架不仅包括了线程池的管理,还提供了线程工厂、队列以及拒绝策略等,Executor 框架让并发编程变得更加简单。

3.3、ThreadPoolExecutor创建线程池

ThreadPoolExecutor 是线程池的核心实现。线程的创建和终止需要很大的开销,线程池中预先提供了指定数量的可重用线程,所以使用线程池会节省系统资源,并且每个线程池都维护了一些基础的数据统计,方便线程的管理和监控。

通过下面的demo来了解ThreadPoolExecutor创建线程的过程。

public class TestThreadPool {
	public static void main(String[] args) {
	   ThreadPoolExecutor threadPoolExecutor = 
			   new ThreadPoolExecutor(3, 6, 5, TimeUnit.SECONDS, new LinkedBlockingDeque<>(5));
	   ExecutorCompletionService<String> executorCompletionService = 
			   new ExecutorCompletionService<>(threadPoolExecutor);
       for (int i = 0; i < 20; i++) {
			try {
              executorCompletionService.submit(()-> {  
					try {  
						//System.out.println("---");  
						Thread.sleep(3000);  
					} catch (InterruptedException e) {  
						e.printStackTrace();  
					}  
				},"testtask"+i);
				
                 System.out.print(" New task: testtask" + i);
                 System.out.print(" ActiveCount: " + threadPoolExecutor.getActiveCount());
                 System.out.print(" poolSize: " + threadPoolExecutor.getPoolSize());
                 System.out.print(" queueSize: " + threadPoolExecutor.getQueue().size());
                 System.out.println(" taskCount: " + threadPoolExecutor.getTaskCount());
           } catch (RejectedExecutionException e) {
                 System.out.println("Reject:" + i);
           }
           try {
              Thread.sleep(200);
           } catch (InterruptedException e) {
              e.printStackTrace();
           }
        }
	   threadPoolExecutor.shutdown();
	}
}
3.4、Executor 框架的继承关系

Java 中的线程池核心实现类是 ThreadPoolExecutor,先通过 JDK 1.8 中 ThreadPoolExecutor 的 UML 类图,了解下 ThreadPoolExecutor 的继承关系。

img

ThreadPoolExecutor 实现的顶层接口是 Executor,顶层接口 Executor 提供了一种思想:将任务提交和任务执行进行解耦。用户无需关注如何创建线程,如何调度线程来执行任务,用户只需提供 Runnable 对象,将任务的运行逻辑提交到执行器(Executor)中,由 Executor 框架完成线程的调配和任务的执行部分。ExecutorService 接口增加了一些能力:

  1. 扩充执行任务的能力,补充可以为一个或一批异步任务生成 Future 的方法;
  2. 提供了管控线程池的方法,比如停止线程池的运行。

AbstractExecutorService 则是上层的抽象类,将执行任务的流程串联了起来,保证下层的实现只需关注一个执行任务的方法即可。最下层的实现类 ThreadPoolExecutor 实现最复杂的运行部分,ThreadPoolExecutor 将会一方面维护自身的生命周期,另一方面同时管理线程和任务,使两者良好的结合从而执行并行任务。

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

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

相关文章

细分 Insight 合作伙伴 2024 年企业技术状况报告

Insight Partners 的团队刚刚发布了 2024 年企业技术状况报告。在 60 幻灯片中有很多东西可以消耗&#xff0c;但我们挑选了应该让我们的观众感兴趣的东西 - 坦率地说&#xff0c;有很多有趣的东西。我将把调查方法的东西留给你使用&#xff0c;但足以说样本量很大&#xff0c;…

dami支付漏洞

使用burpsuite等抓包工具&#xff0c;抓取数据包后&#xff0c;修改数据包中的参数从而达到支付篡改的目的&#xff1b;篡 改的参数&#xff1a;商品ID&#xff0c;购买价格&#xff0c;购买数量&#xff0c;手机号码&#xff0c;订单D&#xff0c;支付状态 常见漏洞利用手段…

国家网络身份个人认证方法

申领网络身份认证后&#xff0c;用户会得到一张虚拟的“网络身份证”&#xff0c;它可以向需要实名认证的互联网平台进行认证&#xff0c;不再需要输入姓名和身份证号等信息。 申请方式&#xff1a;各手机应用平台搜索国家网络身份认证即可&#xff08;必须支持NFC才能申请&am…

AI产品经理必备:什么是LLM,有什么优劣势

LLM&#xff08;Large Language Model大型语言模型&#xff09;是一种人工智能技术&#xff0c;能够理解和生成自然语言文本。LLM可以应用于多种场景&#xff0c;包括自然语言理解、文本生成、机器翻译、对话系统、问答系统、文本摘要、情感分析等。可以帮助人们快速生成文章、…

c# 构造器的声明与调用

在C#中&#xff0c;构造器&#xff08;Constructor&#xff09;是一种特殊类型的函数&#xff0c;用于初始化类的新实例。构造器的名字必须与类名完全相同&#xff0c;并且没有返回类型&#xff0c;甚至连void也不行。 当创建类的一个新实例时&#xff0c;构造器会自动被调用。…

全球轻型电动轮椅市场规划预测:未来六年CAGR为7.3%

随着全球人口老龄化的加剧和消费者对便捷、高效出行工具的需求增加&#xff0c;轻型电动轮椅作为提升行动不便人士生活质量的重要工具&#xff0c;正逐渐受到市场的广泛关注。本文旨在通过深度分析轻型电动轮椅行业的各个维度&#xff0c;揭示行业发展趋势和潜在机会。 【市场…

StudyStudyStudy第十六天(2024.8.2)

1.代理模式 代理模式分为静态代理和动态代理 代理模式&#xff0c;就是在想要执行的代码之前或之后添加代码块&#xff0c;并不会破坏原有的代码结构。可以理解成加上了一个访问层 1.静态代理 创建一个接口Shopping public interface Shopping {void shopping(); }创建一个…

笔记:唐老师讲电赛之唐老师讲电子器件(1)电阻 参数与选型

电阻 a . 精度 电阻----运放中的电阻要选精度高的&#xff0c;一般0.1% 若在设计电路中电路参数由某个电阻决定&#xff0c;则需要选取高精度电阻。例如&#xff0c;反向放大器等对于反馈系数、增益等参数完全由电阻决定的&#xff0c;则需要选取精度较高的电阻&#xff0c;…

PDF文件点击打印无反应?是何原因造成能解决吗?

PDF无法打印怎么处理&#xff1f;在我们工作中&#xff0c;经常会遇见各种各样的文件问题&#xff0c;当我们想要将PDF文件打印出来纸质版使用&#xff0c;却不知什么原因&#xff0c;显示PDF无法打印&#xff0c;这时应该怎么处理呢&#xff1f; 一般情况下&#xff0c;PDF文件…

园区运营管理系统是如何提升园区管理水平和运营效率的?

随着大数据、人工智能等新一代信息技术的迅猛发展&#xff0c;园区运营管理系统逐渐成为提升园区运营效率的重要工具。园区运营管理系统涵盖了运营监测、企业管理、企业服务、项目管理、资产管理、智能办公、物业管理、集成监控等核心功能&#xff0c;利用这些功能可以大幅提升…

Ecovadis认证标准|Ecovadis认证是什么

三分钟内&#xff0c;让我们深入探索Ecovadis认证的广阔世界&#xff0c;这是一场绿色革命中的璀璨明珠&#xff0c;引领着全球企业迈向可持续发展的新纪元。 Ecovadis认证&#xff0c;犹如绿色经济浪潮中的一盏明灯&#xff0c;它不仅照亮了企业责任与环保实践的融合之路&…

动态申请的二维数组不是连续的

在初学阶段&#xff0c;曾学过二维数组的内存空间是连续分布的&#xff0c;参考下图&#xff1a; 后来由于动态申请二维数组的方法不常用&#xff08;一般都是用vector或者直接定义一个二维数组&#xff09;&#xff0c;所以就一直以为二维数组的内存空间是连续的。但实际上&a…

OpenCV仿射变换实现图像扭曲与旋转

目录 1. 仿射变换 2. 仿射变换的求解 3. 代码实现 3.1 图像扭曲 3.2 图像旋转 参考内容 1. 仿射变换 仿射变换是一种可以表达为乘以一个矩阵&#xff08;线性变换&#xff09;再加上一个向量&#xff08;平移&#xff09;的变换。在几何中&#xff0c;就是将一个向量空间…

天空NFT源码:数字藏品交易平台,铸造市场转售盲盒商城系统,附搭建教程和视频

&#x1f31f;【火热NFT数藏交易平台源码限时&#xff01;】&#x1f680; &#x1f47e; 想进军NFT市场却苦于无门&#xff1f; &#x1f3a8; 梦想拥有自己的数字藏品平台&#xff1f; &#x1f6e0;️ 寻找全方位、无加密、易搭建的NFT解决方案&#xff1f; &#x1f525;…

postman查询单条数据Get方法,无任何输出,idea后端也没有任何数据和提示的解决方法

问题描述&#xff1a; 正常使用postman测试&#xff0c;输入内容没有错误&#xff0c;但是却没有任何消息 后端也是&#xff0c;没有任何消息&#xff1a; 解决方法&#xff1a; 问题的原因主要是因为postman&#xff1a; 我们只需要新建一个页面&#xff0c;把刚才的查询语…

Spring Controller接口地址的骚玩法,很有用!

一&#xff0c;背景 项目里有一个接口需要对外提供&#xff0c;对方的解析方式有不同的方式&#xff0c;一个是使用流行的json格式&#xff0c;另外一个却是老系统&#xff0c;只能用xml格式&#xff0c;但是接口内部的实现逻辑是完全一样的&#xff0c;为了适配更多调用方的需…

AS400==创建主机,使用客户端连接上主机

因为AS400基于的CPU架构和自己用的PC不一致&#xff0c;所以要么自己买台AS400主机/上云服务买一台&#xff0c;或者去些网站免费申请一台。 申请地址 PUB400.COM - Your public IBM i server 注册成功后获取到账号密码 然后下载客户端 TN5250 Terminal Emulation for Win…

10. 计算机网络HTTP协议

1. 前言 无论是作为后端开发、前端开发、测试开发程序员或者是运维人员,在面试过程中,大概率都会被问到 HTTP 协议相关题目。 因为伴随着 2010 年之后移动互联网在全世界的高速发展,各种各样的浏览器(Chrome、FireFox、Safari 等)层出不穷,也诞生了诸多服务端开发的语言…

全志Tina_NPU开发部署说明

1 前言 1.1 读者对象 本文档&#xff08;本指南&#xff09;主要适用于以下人员&#xff1a; • 技术支持工程师 • 软件开发工程师 • AI 应用案客户 2 正文 2.1 NPU 开发简介 • 支持int8/uint8/int16 量化精度&#xff0c;运算性能可达1TOPS. • 相较于GPU 作为AI …

关于CSDN登录失效,频繁弹到登录界面

典型特征&#xff1a; 访问其它任何网站都没问题&#xff0c;就是访问任何一个和CSDN有关的网页都会弹到登录界面&#xff0c;且提示认证失败或者Cookies失效等。 刚才我照旧打开CSDN时&#xff0c;CSDN弹出重定向失败&#xff0c;提到了Cookies问题&#xff0c;在个人中心也弹…