【愚公系列】2023年10月 Java教学课程 074-线程池

2023-10-18 13:50

本文主要是介绍【愚公系列】2023年10月 Java教学课程 074-线程池,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在这里插入图片描述

🏆 作者简介,愚公搬代码
🏆《头衔》:华为云特约编辑,华为云云享专家,华为开发者专家,华为产品云测专家,CSDN博客专家,阿里云专家博主,腾讯云优秀博主,掘金优秀博主,51CTO博客专家等。
🏆《近期荣誉》:2022年CSDN博客之星TOP2,2022年华为云十佳博主等。
🏆《博客内容》:.NET、Java、Python、Go、Node、前端、IOS、Android、鸿蒙、Linux、物联网、网络安全、大数据、人工智能、U3D游戏、小程序等相关领域知识。
🏆🎉欢迎 👍点赞✍评论⭐收藏

文章目录

  • 🚀一、线程池
    • 🔎1.线程生命周期介绍
    • 🔎2.线程生命周期案例
      • 🦋2.1 线程的生命周期-练习一
      • 🦋2.2 线程的生命周期-练习二
      • 🦋2.3 线程的生命周期-练习三
    • 🔎3.线程池介绍
    • 🔎4.线程池实现的API、参数说明
    • 🔎5.线程池处理Runnable任务
    • 🔎6.线程池处理Callable任务
    • 🔎7.Executors工具类实现线程池
  • 🚀感谢:给读者的一封信


🚀一、线程池

🔎1.线程生命周期介绍

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。线程对象在不同的时期有不同的状态。

Java中的线程存在6种状态,每种线程状态的含义如下:

线程状态具体含义
NEW一个尚未启动的线程的状态。也称之为初始状态、开始状态。线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread()只有线程象,没有线程特征。
RUNNABLE当我们调用线程对象的start方法,那么此时线程对象进入了RUNNABLE状态。那么此时才是真正的在JVM进程中创建了一个线程,线程一经启动并不是立即得到执行,线程的运行与否要听令与CPU的调度,那么我们把这个中间状态称之为可执行状态(RUNNABLE)也就是说它具备执行的资格,但是并没有真正的执行起来而是在等待CPU的度。
BLOCKED当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。
WAITING一个正在等待的线程的状态。也称之为等待状态。造成线程等待的原因有两种,分别是调用Object.wait()、join()方法。处于等待状态的线程,正在等待其他线程去执行一个特定的操作。例如:因为wait()而等待的线程正在等待另一个线程去调用notify()或notifyAll();一个因为join()而等待的线程正在等待另一个线程结束。
TIMED_WAITING一个在限定时间内等待的线程的状态。也称之为限时等待状态。造成线程限时等待状态的原因有三种,分别是:Thread.sleep(long),Object.wait(long)、join(long)。
TERMINATED一个完全运行完成的线程的状态。也称之为终止状态、结束状态

各个状态的转换,如下图所示:

在这里插入图片描述

🔎2.线程生命周期案例

🦋2.1 线程的生命周期-练习一

目的 : 本案例主要演示TIME_WAITING的状态转换。

需求: 编写一段代码,依次显示一个线程的这些状态:NEW -> RUNNABLE -> TIME_WAITING -> RUNNABLE -> TERMINATED

为了简化我们的开发,本次我们使用匿名内部类结合lambda表达式的方式使用多线程。

代码实现

public class ThreadStateDemo01 {public static void main(String[] args) throws InterruptedException {//定义一个内部线程Thread thread = new Thread(() -> {System.out.println("2.执行thread.start()之后,线程的状态:" + Thread.currentThread().getState());try {//休眠100毫秒Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("4.执行Thread.sleep(long)完成之后,线程的状态:" + Thread.currentThread().getState());});//获取start()之前的状态System.out.println("1.通过new初始化一个线程,但是还没有start()之前,线程的状态:" + thread.getState());//启动线程thread.start();//休眠50毫秒Thread.sleep(50);//因为thread1需要休眠100毫秒,所以在第50毫秒,thread处于sleep状态System.out.println("3.执行Thread.sleep(long)时,线程的状态:" + thread.getState());//thread1和main线程主动休眠150毫秒,所以在第150毫秒,thread早已执行完毕Thread.sleep(100);System.out.println("5.线程执行完毕之后,线程的状态:" + thread.getState() + "\n");}}

控制台输出

1.通过new初始化一个线程,但是还没有start()之前,线程的状态:NEW
2.执行thread.start()之后,线程的状态:RUNNABLE
3.执行Thread.sleep(long)时,线程的状态:TIMED_WAITING
4.执行Thread.sleep(long)完成之后,线程的状态:RUNNABLE
5.线程执行完毕之后,线程的状态:TERMINATED

🦋2.2 线程的生命周期-练习二

目的 : 本案例主要演示WAITING的状态转换。

需求 : 编写一段代码,依次显示一个线程的这些状态:NEW -> RUNNABLE -> WAITING -> RUNNABLE -> TERMINATED

代码实现 :

public class ThreadStateDemo02 {public static void main(String[] args) throws InterruptedException {//定义一个对象,用来加锁和解锁Object obj = new Object();//定义一个内部线程Thread thread1 = new Thread(() -> {System.out.println("2.执行thread.start()之后,线程的状态:" + Thread.currentThread().getState());synchronized (obj) {try {//thread1需要休眠100毫秒Thread.sleep(100);//thread1100毫秒之后,通过wait()方法释放obj对象是锁obj.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("4.被object.notify()方法唤醒之后,线程的状态:" + Thread.currentThread().getState());});//获取start()之前的状态System.out.println("1.通过new初始化一个线程,但是还没有start()之前,线程的状态:" + thread1.getState());//启动线程thread1.start();//main线程休眠150毫秒Thread.sleep(150);//因为thread1在第100毫秒进入wait等待状态,所以第150秒肯定可以获取其状态System.out.println("3.执行object.wait()时,线程的状态:" + thread1.getState());//声明另一个线程进行解锁new Thread(() -> {synchronized (obj) {//唤醒等待的线程obj.notify();}}).start();//main线程休眠10毫秒等待thread1线程能够苏醒Thread.sleep(10);//获取thread1运行结束之后的状态System.out.println("5.线程执行完毕之后,线程的状态:" + thread1.getState() + "\n");}}

控制台输出结果

1.通过new初始化一个线程,但是还没有start()之前,线程的状态:NEW
2.执行thread.start()之后,线程的状态:RUNNABLE
3.执行object.wait()时,线程的状态:WAITING
4.被object.notify()方法唤醒之后,线程的状态:RUNNABLE
5.线程执行完毕之后,线程的状态:TERMINATED

🦋2.3 线程的生命周期-练习三

目的 : 本案例主要演示BLOCKED的状态转换。

需求 : 编写一段代码,依次显示一个线程的这些状态:NEW -> RUNNABLE -> BLOCKED -> RUNNABLE -> TERMINATED

public class ThreadStateDemo03 {public static void main(String[] args) throws InterruptedException {//定义一个对象,用来加锁和解锁Object obj2 = new Object();//定义一个线程,先抢占了obj2对象的锁new Thread(() -> {synchronized (obj2) {try {Thread.sleep(100);              //第一个线程要持有锁100毫秒obj2.wait();                          //然后通过wait()方法进行等待状态,并释放obj2的对象锁} catch (InterruptedException e) {e.printStackTrace();}}}).start();//定义目标线程,获取等待获取obj2的锁Thread thread = new Thread(() -> {System.out.println("2.执行thread.start()之后,线程的状态:" + Thread.currentThread().getState());synchronized (obj2) {try {Thread.sleep(100);              //thread3要持有对象锁100毫秒obj2.notify();                        //然后通过notify()方法唤醒所有在ojb2上等待的线程继续执行后续操作} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("4.阻塞结束后,线程的状态:" + Thread.currentThread().getState());});//获取start()之前的状态System.out.println("1.通过new初始化一个线程,但是还没有thread.start()之前,线程的状态:" + thread.getState());//启动线程thread.start();//先等100毫秒Thread.sleep(50);//第一个线程释放锁至少需要100毫秒,所以在第50毫秒时,thread正在因等待obj的对象锁而阻塞System.out.println("3.因为等待锁而阻塞时,线程的状态:" + thread.getState());//再等300毫秒Thread.sleep(300);//两个线程的执行时间加上之前等待的50毫秒总共是250毫秒,所以第300毫秒,所有的线程都已经执行完毕System.out.println("5.线程执行完毕之后,线程的状态:" + thread.getState());}}

控制台输出结果

1.通过new初始化一个线程,但是还没有thread.start()之前,线程的状态:NEW
2.执行thread.start()之后,线程的状态:RUNNABLE
3.因为等待锁而阻塞时,线程的状态:BLOCKED
4.阻塞结束后,线程的状态:RUNNABLE
5.线程执行完毕之后,线程的状态:TERMINATED

🔎3.线程池介绍

概述 :

线程池是一种预先创建一定数量的线程,这些线程在系统启动时就被创建并保存在一个池中。当需要执行任务时,可以直接从池中取出一个线程来执行任务,这样可以避免频繁创建和销毁线程所带来的性能开销。

线程池通常包含以下几个组成部分:

  1. 任务队列:用于存放待执行的任务。

  2. 线程池管理器:用于管理线程池的创建、初始化、销毁等操作。

  3. 工作线程:线程池中的线程,用于执行任务。

  4. 执行任务:线程池中的任务,可以是代码块、函数、方法等。

线程池的优点在于降低了线程创建、销毁的开销,可以重复利用已创建的线程,提高系统的响应速度和处理能力。同时,线程池还能够限制并发线程的数量,防止由于线程数量过多而导致系统资源耗尽的问题。

线程池的设计思路 :

线程池是一种用来管理和复用线程以减少线程创建和销毁的开销的技术。它的设计思路可以概括为以下几个步骤:

  1. 创建线程池:创建一个线程池,包括线程池大小、等待队列、线程拒绝策略等参数。

  2. 初始化线程池:根据线程池大小,创建一定数量的线程,并将它们存放在等待队列中,以备处理任务。

  3. 提交任务:当有任务需要处理时,将任务提交到等待队列中,等待线程池中的线程进行处理。

  4. 线程执行任务:线程池中的线程从等待队列中取出任务并执行。

  5. 处理线程中的异常:如果线程在执行任务时出现异常,需要进行异常处理以避免线程停止运行。

  6. 完成任务后回收线程:当线程完成任务后,将线程返回到线程池中以便于下次任务处理。

  7. 关闭线程池:当线程池不再需要时,需要关闭线程池,停止线程的执行,并且释放线程池中的资源。

在这里插入图片描述

🔎4.线程池实现的API、参数说明

Java中线程池的实现是通过java.util.concurrent包中的ExecutorService接口及其子类来实现的。常用的线程池实现类有ThreadPoolExecutor和ScheduledThreadPoolExecutor。

下面是ThreadPoolExecutor的构造方法及参数说明:

public ThreadPoolExecutor(int corePoolSize, // 线程池核心线程数int maximumPoolSize, // 线程池最大线程数long keepAliveTime, // 线程池中非核心线程的闲置超时时间TimeUnit unit, // keepAliveTime的时间单位BlockingQueue<Runnable> workQueue, // 工作队列,存储还未执行的任务ThreadFactory threadFactory, // 线程工厂,用于创建新线程RejectedExecutionHandler handler) // 拒绝策略,用于处理队列已满,无法执行新任务的情况

常用参数说明:

  • corePoolSize:线程池维护的核心线程数,可以保持常驻的线程数,即使线程没有任务也不会销毁,除非设置了allowCoreThreadTimeOut参数。
  • maximumPoolSize:线程池允许创建的最大线程数,当核心线程池已满时,新的任务会被加入到工作队列中,并且当工作队列也满了并且还有新的任务加入时,才会创建新的线程来执行任务,直到线程数达到了maximumPoolSize限制。
  • keepAliveTime:非核心线程在闲置时间超过keepAliveTime时会被销毁,直到线程数量收缩到corePoolSize。如果调用了allowCoreThreadTimeOut,则所有的线程都会根据keepAliveTime的值进行超时销毁。
  • unit:keepAliveTime的时间单位,可选值包括TimeUnit.SECONDS, TimeUnit.MILLISECONDS, TimeUnit.MINUTES, TimeUnit.HOURS等。
  • workQueue:任务队列,用于存储待执行的任务,常用的实现类有:ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue等。
  • threadFactory:用于创建新的线程,默认的实现是默认的线程工厂DefaultThreadFactory,可以自定义线程工厂,来设置线程名称、优先级等信息。
  • handler:拒绝策略,当工作队列已满或线程池已经达到最大线程数,未被执行的任务就会被拒绝,并交给指定的拒绝策略来处理,常用的策略有:AbortPolicy(抛出RejectedExecutionException),CallerRunsPolicy(在调用者线程中直接执行),DiscardOldestPolicy(丢弃队列头部最早的任务),DiscardPolicy(直接丢弃任务)。

ScheduledThreadPoolExecutor的构造方法及参数说明基本和ThreadPoolExecutor相同,只是多了一个ScheduledExecutorService的特点,即可以支持定时和周期性任务的执行,常用的方法包括schedule、scheduleAtFixedRate、scheduleWithFixedDelay等。

🔎5.线程池处理Runnable任务

具体步骤如下:

  1. 创建ThreadPoolExecutor对象,可以使用ThreadPoolExecutor构造器或者通过Executors工厂类提供的newCachedThreadPool()、newFixedThreadPool()、newSingleThreadExecutor()等方法创建ThreadPoolExecutor对象。

  2. 创建Runnable对象,重写run()方法。

  3. 使用execute()方法将Runnable任务提交至线程池执行。

  4. 在任务执行完成后,需要调用ThreadPoolExecutor对象的shutdown()方法关闭线程池。

下面是一个使用ThreadPoolExecutor处理Runnable任务的示例代码:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;public class ThreadPoolExecutorDemo {public static void main(String[] args) {// 创建ThreadPoolExecutor对象ThreadPoolExecutor executor = (ThreadPoolExecutor) Executors.newFixedThreadPool(3);// 创建Runnable任务Runnable task1 = new MyRunnable("Task 1");Runnable task2 = new MyRunnable("Task 2");Runnable task3 = new MyRunnable("Task 3");// 提交任务executor.execute(task1);executor.execute(task2);executor.execute(task3);// 关闭线程池executor.shutdown();}static class MyRunnable implements Runnable {private String taskName;public MyRunnable(String taskName) {this.taskName = taskName;}@Overridepublic void run() {System.out.println("Task " + taskName + " is running on thread " + Thread.currentThread().getName());try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("Task " + taskName + " is completed.");}}
}

在上述示例代码中,我们使用了Executors工厂类提供的newFixedThreadPool()方法创建了一个大小为3的线程池ThreadPoolExecutor对象。然后创建了3个Runnable任务,并使用execute()方法将任务提交至线程池执行。在任务执行完成后,需要调用ThreadPoolExecutor对象的shutdown()方法关闭线程池。

🔎6.线程池处理Callable任务

在Java中,ThreadPoolExecutor类是一种用于执行线程池任务的工具类。它使用一个线程池来管理和分配线程,并且在执行任务时,可以控制线程的数量,以及限制最大的并发任务数。

使用ThreadPoolExecutor类处理Callable任务的步骤如下:

  1. 创建一个ThreadPoolExecutor对象,并设置线程池的相关参数,例如核心线程数、最大线程数、等待队列容量等。

  2. 创建一个Callable任务,实现Callable接口中的call()方法。

  3. 使用线程池对象的submit()方法提交Callable任务,并返回一个Future对象。

  4. 使用Future对象的get()方法等待Callable任务执行完成,并获取任务的返回值。

下面是一个示例代码,演示了如何使用ThreadPoolExecutor类处理Callable任务:

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;public class ThreadPoolExecutorDemo {public static void main(String[] args) {// 1、创建一个线程池对象/**public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue,ThreadFactory threadFactory,RejectedExecutionHandler handler)*/// ExecutorService pool = new ThreadPoolExecutor(3,5,//         6, TimeUnit.SECONDS, new ArrayBlockingQueue<>(5)// , Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());// 创建线程池对象,设置参数ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 4, 10, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10));// 提交Callable任务Future<String> future = executor.submit(new MyCallable());try {// 等待任务完成,并获取返回值String result = future.get();System.out.println("任务执行结果:" + result);} catch (InterruptedException | ExecutionException e) {e.printStackTrace();}// 关闭线程池executor.shutdown();}static class MyCallable implements Callable<String> {@Overridepublic String call() throws Exception {// 执行任务Thread.sleep(1000);return "任务执行完成";}}
}

在上面的示例中,我们创建了一个ThreadPoolExecutor对象,并设置核心线程数为2,最大线程数为4,等待队列容量为10。然后,我们创建了一个MyCallable对象,实现了Callable接口中的call()方法,用于模拟一个任务需要执行1秒钟的时间。最后,我们使用线程池对象的submit()方法提交Callable任务,并返回一个Future对象,调用Future对象的get()方法等待任务执行完成,并获取返回值。

需要注意的是,在程序结束后必须手动关闭线程池,否则会导致程序一直处于运行状态。可以使用executor.shutdown()方法来关闭线程池。

🔎7.Executors工具类实现线程池

Executors工具类提供了多种不同类型的线程池,每种线程池都有其独特的特点。下面对参数进行简要介绍:

  1. newFixedThreadPool(int nThreads):创建一个含有固定线程数的线程池,不会创建新的线程,如果所有线程都在执行任务,则任务将在队列中等待。核心线程池大小为nThreads,最大线程池大小也为nThreads,线程空闲时间为0,使用无界的LinkedBlockingQueue作为等待队列。

  2. newCachedThreadPool():创建一个可以根据需要创建新线程的线程池。核心线程池大小为0,最大线程池大小为Integer.MAX_VALUE,线程空闲时间为60秒,使用SynchronousQueue作为等待队列,适用于执行大量短期异步任务的场景。

  3. newSingleThreadExecutor():创建一个只有一个线程的线程池,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行,并且在该线程中序列化所有提交给它的任务。

  4. newScheduledThreadPool(int corePoolSize):创建一个含有指定线程数的线程池,支持定时以及周期性执行任务的需求。

下面是一个完整的示例,使用Executors创建一个FixedThreadPool并提交10个任务:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class ThreadPoolExample {public static void main(String[] args) {// 创建大小为5的线程池ExecutorService executorService = Executors.newFixedThreadPool(5);for(int i=0; i<10; i++) {executorService.execute(new Task(i));}// 关闭线程池executorService.shutdown();}private static class Task implements Runnable {private final int taskId;public Task(int taskId) {this.taskId = taskId;}@Overridepublic void run() {System.out.println("Task " + taskId + " is running.");}}
}

输出结果:

Task 0 is running.
Task 4 is running.
Task 2 is running.
Task 3 is running.
Task 1 is running.
Task 5 is running.
Task 6 is running.
Task 7 is running.
Task 8 is running.
Task 9 is running.

可以看到,线程池中有5个线程在执行任务,其他任务在等待队列中等待执行。


🚀感谢:给读者的一封信

亲爱的读者,

我在这篇文章中投入了大量的心血和时间,希望为您提供有价值的内容。这篇文章包含了深入的研究和个人经验,我相信这些信息对您非常有帮助。

如果您觉得这篇文章对您有所帮助,我诚恳地请求您考虑赞赏1元钱的支持。这个金额不会对您的财务状况造成负担,但它会对我继续创作高质量的内容产生积极的影响。

我之所以写这篇文章,是因为我热爱分享有用的知识和见解。您的支持将帮助我继续这个使命,也鼓励我花更多的时间和精力创作更多有价值的内容。

如果您愿意支持我的创作,请扫描下面二维码,您的支持将不胜感激。同时,如果您有任何反馈或建议,也欢迎与我分享。

在这里插入图片描述

再次感谢您的阅读和支持!

最诚挚的问候, “愚公搬代码”

这篇关于【愚公系列】2023年10月 Java教学课程 074-线程池的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/232975

相关文章

JVM 的类初始化机制

前言 当你在 Java 程序中new对象时,有没有考虑过 JVM 是如何把静态的字节码(byte code)转化为运行时对象的呢,这个问题看似简单,但清楚的同学相信也不会太多,这篇文章首先介绍 JVM 类初始化的机制,然后给出几个易出错的实例来分析,帮助大家更好理解这个知识点。 JVM 将字节码转化为运行时对象分为三个阶段,分别是:loading 、Linking、initialization

Spring Security 基于表达式的权限控制

前言 spring security 3.0已经可以使用spring el表达式来控制授权,允许在表达式中使用复杂的布尔逻辑来控制访问的权限。 常见的表达式 Spring Security可用表达式对象的基类是SecurityExpressionRoot。 表达式描述hasRole([role])用户拥有制定的角色时返回true (Spring security默认会带有ROLE_前缀),去

浅析Spring Security认证过程

类图 为了方便理解Spring Security认证流程,特意画了如下的类图,包含相关的核心认证类 概述 核心验证器 AuthenticationManager 该对象提供了认证方法的入口,接收一个Authentiaton对象作为参数; public interface AuthenticationManager {Authentication authenticate(Authenti

Spring Security--Architecture Overview

1 核心组件 这一节主要介绍一些在Spring Security中常见且核心的Java类,它们之间的依赖,构建起了整个框架。想要理解整个架构,最起码得对这些类眼熟。 1.1 SecurityContextHolder SecurityContextHolder用于存储安全上下文(security context)的信息。当前操作的用户是谁,该用户是否已经被认证,他拥有哪些角色权限…这些都被保

Spring Security基于数据库验证流程详解

Spring Security 校验流程图 相关解释说明(认真看哦) AbstractAuthenticationProcessingFilter 抽象类 /*** 调用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 决定是否需要进行验证操作。* 如果需要验证,则会调用 #attemptAuthentica

Spring Security 从入门到进阶系列教程

Spring Security 入门系列 《保护 Web 应用的安全》 《Spring-Security-入门(一):登录与退出》 《Spring-Security-入门(二):基于数据库验证》 《Spring-Security-入门(三):密码加密》 《Spring-Security-入门(四):自定义-Filter》 《Spring-Security-入门(五):在 Sprin

Java架构师知识体认识

源码分析 常用设计模式 Proxy代理模式Factory工厂模式Singleton单例模式Delegate委派模式Strategy策略模式Prototype原型模式Template模板模式 Spring5 beans 接口实例化代理Bean操作 Context Ioc容器设计原理及高级特性Aop设计原理Factorybean与Beanfactory Transaction 声明式事物

Java进阶13讲__第12讲_1/2

多线程、线程池 1.  线程概念 1.1  什么是线程 1.2  线程的好处 2.   创建线程的三种方式 注意事项 2.1  继承Thread类 2.1.1 认识  2.1.2  编码实现  package cn.hdc.oop10.Thread;import org.slf4j.Logger;import org.slf4j.LoggerFactory

JAVA智听未来一站式有声阅读平台听书系统小程序源码

智听未来,一站式有声阅读平台听书系统 🌟&nbsp;开篇:遇见未来,从“智听”开始 在这个快节奏的时代,你是否渴望在忙碌的间隙,找到一片属于自己的宁静角落?是否梦想着能随时随地,沉浸在知识的海洋,或是故事的奇幻世界里?今天,就让我带你一起探索“智听未来”——这一站式有声阅读平台听书系统,它正悄悄改变着我们的阅读方式,让未来触手可及! 📚&nbsp;第一站:海量资源,应有尽有 走进“智听

科研绘图系列:R语言扩展物种堆积图(Extended Stacked Barplot)

介绍 R语言的扩展物种堆积图是一种数据可视化工具,它不仅展示了物种的堆积结果,还整合了不同样本分组之间的差异性分析结果。这种图形表示方法能够直观地比较不同物种在各个分组中的显著性差异,为研究者提供了一种有效的数据解读方式。 加载R包 knitr::opts_chunk$set(warning = F, message = F)library(tidyverse)library(phyl