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

大家好,我是栗筝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 {@Overridepublic 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.ppmy.cn/news/1504433.html

相关文章

前端自定义组件+自建npm缓存服务器

自建 npm 缓存服务 安装 verdaccio npm install -global verdaccio启动 verdaccio verdaccio默认情况下&#xff0c;会在本地启动一个服务 http://localhost:4873/ 开发公共组件 在你的工程目录下执行 npm init -y &#xff0c;相关配置自己定义index.ts 中开发组件逻辑 …

【WPF开发】如何将工程打包成单独的EXE安装包

一、安装NSIS与HM NIS Edit 1、下载和安装NSIS NSIS官网 2、下载和安装HM NIS Edit HM NIS Edit官网 点击下载后等待几秒&#xff0c;就会弹出下载提示 双击下载的安装包&#xff0c;点击“OK” 点击“下一步” 点击“我接受” 更改路径后&#xff0c;点击安装即可 二、打包软…

82.WEB渗透测试-信息收集-框架组件识别利用(6)

免责声明&#xff1a;内容仅供学习参考&#xff0c;请合法利用知识&#xff0c;禁止进行违法犯罪活动&#xff01; 内容参考于&#xff1a; 易锦网校会员专享课 上一个内容&#xff1a;81.WEB渗透测试-信息收集-框架组件识别利用&#xff08;5&#xff09; log4j/log4j2&…

测试人生 | 招聘严峻期从面试无力感,到一天2个offer的一些经验分享(内附美团、字节、快手等面试题)

本人是霍格沃兹北京测试开发学社线下3期学员&#xff0c;拥有3年测试工作经验&#xff0c;之前一直在某大厂外包做软件测试&#xff0c;期间主要是以功能测试为主。 经过一个月的高强度找工作奋战&#xff0c;最终拿下了3家公司offer&#xff0c;选择了一家自己很满意的公司。…

③分析胃癌组蛋白脱乙酰酶HDS模型-SeuratHDS细胞比例

目录 文献Seurat处理步骤 ①HDS分析步骤 ②原文献数据处理 加载单细胞Seurat 换算每个样本每种细胞比例 比较两个分组比例 ①比例计算 ②绘图 单种细胞比例亚组间差异 ①添加分组信息 ②循环作图 文献Seurat处理步骤 组蛋白脱乙酰酶介导的胃癌肿瘤微环境特征及协同…

机器学习笔记 - python学习记录三 Python中的矢量化

当我们必须处理大量数据集时,计算上非最优的函数可能会成为算法的巨大瓶颈,最终导致模型运行时间过长。为了确保代码计算效率高,我们将使用矢量化。任何算法执行的时间复杂度都非常重要,决定了应用程序是否可靠。在实时输出应用方面,尽可能在最佳时间内运行大型算法非常重…

【从相邻元素对还原数组】python刷题记录

R3-图篇 饭前一道题 思路&#xff1a; 单向构造 class Solution:def restoreArray(self, adjacentPairs: List[List[int]]) -> List[int]:mnlen(adjacentPairs)#n表示数组元素总数n1#统计dictdefaultdict(int)#存储关系哈希表hashmapdefaultdict(list)for a,b in adjacent…

E25.【C语言】练习:修改二进制序列的指定位

十进制13-->二进制01101 现要求二进制序列的第5位修改为1&#xff0c;再改成0 复习&#xff1a;逻辑运算 非&#xff08;NOT&#xff09;&#xff08;C语言&#xff1a;~&#xff09; x0&#xff0c;NOT x-->1&#xff1b;x1&#xff0c;NOT x-->0 与&#xff08;…

day12 多线程

目录 1.概念相关 1.1什么是线程 1.2什么是多线程 2.创建线程 2.1方式一&#xff1a;继承Thread类 2.1.1实现步骤 2.1.2优缺点 2.1.3注意事项 2.2方式二&#xff1a;实现Runnable接口 2.2.1实现步骤 2.2.2优缺点 2.2.3匿名内部类写法 2.3方式三&#xff1a;实现cal…

JAVA游戏源码:仙剑|大学生练手项目

学习java朋友们&#xff0c;福利来了&#xff0c;今天小编给大家带来了一款仙剑源码。注意&#xff1a;此源码仅供学习使用!! 源码搭建和讲解 启动main入口&#xff1a; //************************************************************************ // ************完整源码…

一步步搭建Web自动化测试框架

测试框架的设计有两种思路&#xff0c;一种是自底向上&#xff0c;从脚本逐步演变完善成框架&#xff0c;这种适合新手了解框架的演变过程。另一种则是自顶向下&#xff0c;直接设计框架结构和选取各种问题的解决方案&#xff0c;这种适合有较多框架事件经验的人。本章和下一张…

jenkins集成jmeter

jenkins 安装插件HTML Publisher startup trigger Groovy 脚本介绍 cd /app/jmeter rm -rf result.jtl jmeter.log report mkdir -p report sh /app/jmeter/apache-jmeter-5.6.3/bin/jmeter.sh -n -t test.jmx -l result.jtl -e -o ./report-n: 表示以非 GUI 模式运行 JMete…

一拖三无线充底座-带给你极致的便利生活

随着科技的不断进步&#xff0c;无线充电技术已经逐渐渗透到我们日常生活的方方面面&#xff0c;一拖三无线充底座作为其中的佼佼者&#xff0c;以其高效、便捷的特点受到广大用户的青睐。本文将从电磁感应原理、多线圈设计、频率匹配、电能传输、功率分配以及充电管理六个方面…

尚硅谷电商实时数仓笔记-《二》数仓建模概述

上一篇&#xff1a; 尚硅谷电商实时数仓笔记-《一》数仓概述-CSDN博客 二、数仓建模概述 2.1 数据仓库建模的意义 如果把数据看作图书馆里的书&#xff0c;我们希望看到它们在书架上分门别类地放置&#xff1b; 如果把数据看作城市的建筑&#xff0c;我们希望城市规划布局合理…

javascript的隐式转换和显式转换

javascript隐式转换 其中号两边只要有一个是字符&#xff0c;都会把另外一个转为字符 如果是加号以外的算术运算符&#xff0c;比如-*都会把数据转换为数字类型。 比如12得出结果是字符型12 2-1得出结果是数字型1 23得到结果是数字型23 显式转换&#xff0c;比如说要把一…

基于ThinkPHP开发的校园跑腿社区小程序系统源码,包含前后端代码

基于ThinkPHP开发的校园跑腿社区小程序系统源码&#xff0c;包含前后端代码 最新独立版校园跑腿校园社区小程序系统源码 | 附教程 测试环境&#xff1a;NginxPHP7.2MySQL5.6 多校版本&#xff0c;多模块&#xff0c;适合跑腿&#xff0c;外卖&#xff0c;表白&#xff0c;二…

每日新闻掌握【2024年8月1日 星期四】

2024年8月1日 星期四 农历六月廿七 今天是中国人民解放军建军纪念日 TOP大新闻 男乒运动员王楚钦回应巴黎奥运会爆冷出局 7月31日&#xff0c;巴黎奥运会乒乓球男子单打1/16决赛爆出冷门&#xff0c;头号种子中国选手王楚钦2比4不敌瑞典选手莫雷加德&#xff0c;无缘16强。 …

Turbo Boost 禁用

最近在做OAI NR的时候关闭CPU 睿频的时候出了一些问题&#xff0c;这里我把我找到的资料记录一下&#xff1a; 禁用 Turbo Boost 的过程可能会因不同的 BIOS/UEFI 和操作系统设置而有所不同。以下是一些可能的原因及解决方法&#xff1a; 可能的原因 BIOS/UEFI 设置问题: 你的…

【从零开始一步步学习VSOA开发】快速体验SylixOS

快速体验SylixOS 安装完毕RealEvo-IDE 后&#xff0c;同时也安装了RealEvo-Simulator。RealEvo-Simulator 是一个虚拟运行环境&#xff0c;可以模拟各种体系结构并在其上运行 SylixOS。相比于物理板卡&#xff0c;在 RealEvo-Simulator 进行运行调测更加的方便快捷且成本低廉。…

php 一个极简的类例子

https://andi.cn/page/621627.html