线程,线程池的使用

news2024/11/16 15:50:53

文章目录

    • 线程,线程池的使用
    • 1. 多线程基础
      • 1.1 线程和进程
      • 1.2 多线程的创建
        • 1.2.1 继承Thread类
        • 1.2.2 实现Runnable接口
        • 1.2.3 匿名内部类方式
        • 1.2.4 守护线程
      • 1.3 线程安全
        • 1.3.1 卖票案例
        • 1.3.2 线程同步
    • 2. 线程池的实现方式
      • 2.1 Java提供的四种线程池
      • 2.2 线程池的创建原理
      • 2.3 自定义线程池
    • 3. 线程池在业务中使用
      • 3.1 使用多线程处理excel表中的多个sheet数据
        • 3.1.2 添加EasyPoi依赖
        • 3.1.3 简单导入
        • 3.1.4 多线程导入
        • 3.1.5 线程池导入
      • 3.2 有多个第三方接口调用时
        • 3.2.1 提供两个第三方接口
          • 短信
          • 邮件
          • 串行方式
          • 并行方式
      • 3.3 消费监听时,启动多个线程进行监听
          • 发送端
          • 消费端

线程,线程池的使用

1. 多线程基础

​ 一个采用了多线程技术的应用程序可以更好地利用系统资源。其主要优势在于充分利用了CPU的空闲时间片,可以用尽可能少的时间来对用户的要求做出响应,使得进程的整体运行效率得到较大提高,同时增强了应用程序的灵活性。

​ 更为重要的是,由于同一进程的所有线程是共享同一内存,所以不需要特殊的数据传送机制,不需要建立共享存储区或共享文件,从而使得不同任务之间的协调操作与运行、数据的交互、资源的分配等问题更加易于解决。

1.1 线程和进程

进程:

​ 是指一个内存中运行的应用程序,每个进程都有一个独立的内存空间,一个应用程序可以同时运行多个进程;进程也是程序的一次执行过程,是系统运行程序的基本单位;系统运行一个程序即是一个进程从创建、运行到消亡的过程。

线程:

​ 进程内部的一个独立执行单元;一个进程可以同时并发的运行多个线程,可以理解为一个进程便相当于一个单 CPU 操作系统,而线程便是这个系统中运行的多个任务。

进程与线程的区别:

​ 进程:有独立的内存空间,进程中的数据存放空间(堆空间和栈空间)是独立的,至少有一个线程。

​ 线程:堆空间是共享的,栈空间是独立的,线程消耗的资源比进程小的多。

注意:

  1. 因为一个进程中的多个线程是并发运行的,那么从微观角度看也是有先后顺序的,哪个线程执行完全取决于 CPU 的调度,程序员是不能完全控制的(可以设置线程优先级)。而这也就造成的多线程的随机性。
  2. Java 程序的进程里面至少包含两个线程,主线程也就是 main()方法线程,另外一个是垃圾回收机制线程。每当使用 java 命令执行一个类时,实际上都会启动一个 JVM,每一个 JVM 实际上就是在操作系统中启动了一个 线程,java 本身具备了垃圾的收集机制,所以在 Java 运行时至少会启动两个线程。
  3. 由于创建一个线程的开销比创建一个进程的开销小的多,那么我们在开发多任务运行的时候,通常考虑创建 多线程,而不是创建多进程。

额外

并发和并行

​ 并发: 同时发生事情开始处理 看起来同时执行

​ 并行: 操作系统同时执行多个逻辑 真正的同时

1.2 多线程的创建

创建Maven工程,编写测试类

1.2.1 继承Thread类

​ 第一种继承Thread类 重写run方法

public class Demo1CreateThread  {

    public static void main(String[] args) throws InterruptedException {

        System.out.println("-----多线程创建开始-----");
        // 1.创建一个线程
        CreateThread createThread1 = new CreateThread();
        CreateThread createThread2 = new CreateThread();
        // 2.开始执行线程 注意 开启线程不是调用run方法,而是start方法
        System.out.println("-----多线程创建启动-----");
        createThread1.start();
        createThread2.start();
        System.out.println("-----多线程创建结束-----");
    }

    static class CreateThread extends Thread {
        public void run() {
            String name = Thread.currentThread().getName();
            for (int i = 0; i < 5; i++) {
                System.out.println(name + "打印内容是:" + i);
            }
        }
    }
}

1.2.2 实现Runnable接口

​ 实现Runnable接口,重写run方法

​ 实际上所有的多线程代码都是通过运行Thread的start()方法来运行的。因此,不管是继承Thread类还是实现Runnable接口来实现多线程,最终还是通过Thread的对象的API来控制线程的。

public class Demo2CreateRunnable {

    public static void main(String[] args) {
        System.out.println("-----多线程创建开始-----");
        // 1.创建线程
        CreateRunnable createRunnable = new CreateRunnable();
        Thread thread1 = new Thread(createRunnable);
        Thread thread2 = new Thread(createRunnable);
        // 2.开始执行线程 注意 开启线程不是调用run方法,而是start方法
        System.out.println("-----多线程创建启动-----");
        thread1.start();
        thread2.start();
        System.out.println("-----多线程创建结束-----");
    }

    static class CreateRunnable implements Runnable {

        public void run() {
            String name = Thread.currentThread().getName();
            for (int i = 0; i < 5; i++) {
                System.out.println(name + "的内容:" + i);
            }
        }
    }
}

实现Runnable接口比继承Thread类所具有的优势:

  1. 适合多个相同的程序代码的线程去共享同一个资源。
  2. 可以避免java中的单继承的局限性。
  3. 增加程序的健壮性,实现解耦操作,代码可以被多个线程共享,代码和数据独立。
  4. 线程池只能放入实现Runable或callable类线程,不能直接放入继承Thread的类

1.2.3 匿名内部类方式

​ 使用线程的内匿名内部类方式,可以方便的实现每个线程执行不同的线程任务操作

public class Demo3Runnable {
    public static boolean exit = true;

    public static void main(String[] args) throws InterruptedException {
        new Thread(new Runnable() {
            public void run() {
                String name = Thread.currentThread().getName();
                for (int i = 0; i < 5; i++) {
                    System.out.println(name + "执行内容:" + i);
                }
            }
        }).start();

        new Thread(new Runnable() {
            public void run() {
                String name = Thread.currentThread().getName();
                for (int i = 0; i < 5; i++) {
                    System.out.println(name + "执行内容:" + i);
                }
            }
        }).start();

        Thread.sleep(1000l);
    }
}}

1.2.4 守护线程

Java中有两种线程,一种是用户线程,另一种是守护线程。

用户线程是指用户自定义创建的线程,主线程停止,用户线程不会停止。

守护线程当进程不存在或主线程停止,守护线程也会被停止。

public class Demo4Daemon {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            public void run() {
                while(true) {
                    try {
                        Thread.sleep(10);
                    } catch (Exception e) {
                    }
                    System.out.println("子线程..." );
                }
            }
        });
        
        // 设置线程为守护线程
        //thread.setDaemon(true);
        thread.start();

        for (int i = 0; i < 5; i++) {
            try {
                Thread.sleep(10);
                System.out.println("主线程" + i);
            } catch (Exception e) {

            }
        }

        System.out.println("主线程执行完毕!");
    }
}

1.3 线程安全

1.3.1 卖票案例

​ 如果有多个线程在同时运行,而这些线程可能会同时运行这段代码。程序每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的,反之则是线程不安全的。

public class Demo5Ticket {

    public static void main(String[] args) {
        //创建线程任务对象
        Ticket ticket = new Ticket();
        //创建三个窗口对象
        Thread t1 = new Thread(ticket, "窗口1");
        Thread t2 = new Thread(ticket, "窗口2");
        Thread t3 = new Thread(ticket, "窗口3");

        //卖票
        t1.start();
        t2.start();
        t3.start();
    }

    static class Ticket implements Runnable {

        //Object lock = new Object();
        ReentrantLock lock = new ReentrantLock();
        private int ticket = 10;

        public void run() {
            String name = Thread.currentThread().getName();
            while (true) {
                sell(name);
                if (ticket <= 0) {
                    break;
                }
            }
        }

        private void sell(String name) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (ticket > 0) {
                System.out.println(name + "卖票:" + ticket);
                ticket--;
            }
        }
    }
}

​ 线程安全问题都是由全局变量及静态变量引起的。若每个线程中对全局变量、静态变量只有读操作,而无写 操作,一般来说,这个全局变量是线程安全的;若有多个线程同时执行写操作,一般都需要考虑线程同步, 否则的话就可能影响线程安全。

1.3.2 线程同步

当我们使用多个线程访问同一资源的时候,且多个线程中对资源有写的操作,就容易出现线程安全问题。 要解决上述多线程并发访问一个资源的安全问题,Java中提供了同步机制(synchronized)来解决。

同步代码块

Object lock = new Object(); //创建锁
synchronized(lock){
     //可能会产生线程安全问题的代码
}

同步方法

//同步方法
public synchronized void method(){
   //可能会产生线程安全问题的代码 
}

同步方法使用的是this锁

证明方式: 一个线程使用同步代码块(this明锁),另一个线程使用同步函数。如果两个线程抢票不能实现同步,那么会出现数据错误。

//使用this锁的同步代码块
synchronized(this){
     //需要同步操作的代码
}

Lock锁

Lock lock = new ReentrantLock();
lock.lock();
    //需要同步操作的代码
lock.unlock();

2. 线程池的实现方式

线程池其实是池化技术的应用一种,常见的池化技术还有很多,例如数据库的连接池、Java中的内存池、常量池等等。而为什么会有池化技术呢?程序的运行本质,就是通过使用系统资源(CPU、内存、网络、磁盘等等)来完成信息的处理,比如在JVM中创建一个对象实例需要消耗CPU的和内存资源,如果你的程序需要频繁创建大量的对象,并且这些对象的存活时间短就意味着需要进行频繁销毁,那么很有可能这段代码就成为了性能的瓶颈。总结下来其实就以下几点。

  • 复用相同的资源,减少浪费,减少新建和销毁的成本;
  • 减少单独管理的成本,统一交由"池";
  • 集中管理,减少"碎片";
  • 提高系统响应速度,因为池中有现成的资源,不用重新去创建;

所以池化技术就是为了解决我们这些问题的,简单来说,线程池就是将用过的对象保存起来,等下一次需要这个对象的时候,直接从对象池中拿出来重复使用,避免频繁的创建和销毁。在Java中万物皆对象,那么线程也是一个对象,Java线程是对于操作系统线程的封装,创建Java线程也需要消耗操作系统的资源,因此就有了线程池。但是我们该如何创建呢?

2.1 Java提供的四种线程池

Java为我们提供了四种创建线程池的方法。

  • Executors.newCachedThreadPool:创建可缓存无限制数量的线程池,如果线程中没有空闲线程池的话此时再来任务会新建线程,如果超过60秒此线程无用,那么就会将此线程销毁。简单来说就是忙不来的时候无限制创建临时线程,闲下来再回收
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue < runnable > ());
}
  • Executors.newFixedThreadPool:创建固定大小的线程池,可控制线程最大的并发数,超出的线程会在队列中等待。简单来说就是忙不来的时候会将任务放到无限长度的队列里。
public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue < runnable > ());
}
  • Executors.newSingleThreadExecutor:创建线程池中线程数量为1的线程池,用唯一线程来执行任务,保证任务是按照指定顺序执行
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue < runnable > ()));
}
  • Executors.newScheduledThreadPool:创建固定大小的线程池,支持定时及周期性的任务执行
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue());
}

2.2 线程池的创建原理

我们点击去这四种实现方式的源码中我们可以看到其实它们的底层创建原理都是一样的,只不过是所传的参数不同组成的四个不同类型的线程池。都是使用了ThreadPoolExecutor来创建的。我们可以看一下ThreadPoolExecutor创建所传的参数。

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

那么这些参数都具体代表什么意思呢?

  • corePoolSize:线程池中核心线程数的数量

corePoolSize:核心线程数

线程池维护的最小线程数量,核心线程创建后不会被回收(注意:设置allowCoreThreadTimeout=true后,空闲的核心线程超过存活时间也会被回收)。

大于核心线程数的线程,在空闲时间超过keepAliveTime后会被回收。

线程池刚创建时,里面没有一个线程,当调用 execute() 方法添加一个任务时,如果正在运行的线程数量小于corePoolSize,则马上创建新线程并运行这个任务。

  • maximumPoolSize:在线程池中允许存在的最大线程数

maximumPoolSize:最大线程数

线程池允许创建的最大线程数量。

当添加一个任务时,核心线程数已满,线程池还没达到最大线程数,并且没有空闲线程,工作队列已满的情况下,创建一个新线程并执行。

  • keepAliveTime:当存在的线程数大于corePoolSize,那么会找到空闲线程去销毁,此参数是设置空闲多久的线程才被销毁。

keepAliveTime:空闲线程存活时间

当一个可被回收的线程的空闲时间大于keepAliveTime,就会被回收。

可被回收的线程:

设置allowCoreThreadTimeout=true的核心线程。
大于核心线程数的线程(非核心线程)。

  • unit:时间单位

TimeUnit.NANOSECONDS
TimeUnit.MICROSECONDS
TimeUnit.MILLISECONDS // 毫秒
TimeUnit.SECONDS
TimeUnit.MINUTES
TimeUnit.HOURS
TimeUnit.DAYS

  • workQueue:工作队列,线程池中的当前线程数大于核心线程的话,那么接下来的任务会放入到队列中

存放待执行任务的队列:当提交的任务数超过核心线程数大小后,再提交的任务就存放在工作队列,任务调度时再从队列中取出任务。它仅仅用来存放被execute()方法提交的Runnable任务。工作队列实现了BlockingQueue接口。

JDK默认的工作队列有五种:

ArrayBlockingQueue 数组型阻塞队列:数组结构,初始化时传入大小,有界,FIFO,使用一个重入锁,默认使用非公平锁,入队和出队共用一个锁,互斥。
LinkedBlockingQueue 链表型阻塞队列:链表结构,默认初始化大小为Integer.MAX_VALUE,有界(近似无解),FIFO,使用两个重入锁分别控制元素的入队和出队,用Condition进行线程间的唤醒和等待。
SynchronousQueue 同步队列:容量为0,添加任务必须等待取出任务,这个队列相当于通道,不存储元素。
PriorityBlockingQueue 优先阻塞队列:无界,默认采用元素自然顺序升序排列。
DelayQueue 延时队列:无界,元素有过期时间,过期的元素才能被取出

  • threadFactory:在创建线程的时候,通过工厂模式来生产线程。这个参数就是设置我们自定义的线程创建工厂。
  • handler:如果超过了最大线程数,那么就会执行我们设置的拒绝策略

当线程池线程数已满,并且工作队列达到限制,新提交的任务使用拒绝策略处理。可以自定义拒绝策略,拒绝策略需要实现RejectedExecutionHandler接口。

JDK默认的拒绝策略有四种:

AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
DiscardPolicy:丢弃任务,但是不抛出异常。可能导致无法发现系统的异常状态。
DiscardOldestPolicy:丢弃队列最前面的任务,然后重新提交被拒绝的任务。
CallerRunsPolicy:由调用线程处理该任务。

接下来我们将这些参数合起来看一下他们的处理逻辑是什么。

  1. corePoolSize个任务时,来一个任务就创建一个线程

  2. 如果当前线程池的线程数大于了corePoolSize那么接下来再来的任务就会放入到我们上面设置的workQueue队列中

  3. 如果此时workQueue也满了,那么再来任务时,就会新建临时线程,那么此时如果我们设置了keepAliveTime或者设置了allowCoreThreadTimeOut,那么系统就会进行线程的活性检查,一旦超时便销毁线程

  4. 如果此时线程池中的当前线程大于了maximumPoolSize最大线程数,那么就会执行我们刚才设置的handler拒绝策略

    但是在企业中不建议使用以上的方式进行线程池的创建,容易造成OOM

  • FixedThreadPoolSingleThreadExecutor:这两个线程池的实现方式,我们可以看到它设置的工作队列都是LinkedBlockingQueue,我们知道此队列是一个链表形式的队列,此队列是没有长度限制的,是一个无界队列,那么此时如果有大量请求,就有可能造成OOM
  • CachedThreadPoolScheduledThreadPool:这两个线程池的实现方式,我们可以看到它设置的最大线程数都是Integer.MAX_VALUE,那么就相当于允许创建的线程数量为Integer.MAX_VALUE。此时如果有大量请求来的时候也有可能造成OOM

2.3 自定义线程池

其实线程池大小的设置还是要根据自己业务类型来设置,比如当前任务需要池化的资源的时候,比如数据库的连接池,线程池的长度和资源池的长度会相互的影响。如果每一个任务都需要一个数据库连接,那么连接池的大小就会限制了线程池的有效大小,类似的,当线程池中的任务是连接池的唯一消费者时,那么线程池的大小反而又会限制了连接池的有效大小。

所以我们在项目中如果要使用线程池的话,那么就推荐根据自己项目和机器的情况进行个性化创建线程池。那么这些参数如何设置呢?为了正确的定制线程池的长度,需要理解你的计算机配置、所需资源的情况以及任务的特性。比如部署的计算机安装了多少个CPU?多少的内存?任务主要执行是IO密集型还是CPU密集型?所执行任务是否需要数据库连接这样的稀缺资源?

> 如果你有多个不同类别的任务,它们的行为有很大的差别,那么应该考虑使用多个线程池。这样也能根据每个任务不同定制不同的线程池,也不至于因为一种类型的任务失败而托垮另一个任务。

  • CPU密集型任务:说明包含了大量的运算操作,比如有N个CPU,那么就配置线程池的容量大小为N+1,这样能获得最优的利用率。因为CPU密集型的线程恰好在某时因为发生一个页错误或者因为其他的原因而暂停,刚好有一个额外的线程,可以确保在这种情况下CPU周期不会中断工作。

  • IO密集任务:说明CPU大部分时间都是在等待IO的阻塞操作,那么此时就可以将线程池的容量大小配置的大一些。此时可以根据一些参数进行计算大概你的线程池的数量多少合适。

    • N:CPU的数量
    • U:目标CPU的使用率,0<=U<=1
    • W/C:等待时间与计算时间的比率
    • 那么最优的池的大小就是N*U*(1+W/C)

> 页缺失(英语:Page fault,又名硬错误、硬中断、分页错误、寻页缺失、缺页中断、页故障等)指的是当软件试图访问已映射在虚拟地址空间中,但是当前并未被加载在物理内存中的一个分页时,由中央处理器的内存管理单元所发出的中断

package com.qf.first;

import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ExecutorDemoTicket {
    /**
     * 多个线程同时执行情况下出现超卖问题
     * @param args
     */
    public static void main(String[] args) {
        //创建线程任务对象
        Ticket ticket = new Ticket();
        //创建线程池对象,使用线程池进行线程的创建
        ticket.newCachedThreadPool();
       // ticket.newCachedThreadPool();

//        //卖票
//        t1.start();
//        t2.start();
//        t3.start();
    }

    static class Ticket {

        //ReentrantLock lock = new ReentrantLock();
        private int ticket = 10;
      void newCachedThreadPool() {

         ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,new SynchronousQueue<>());
         threadPoolExecutor.execute(new Runnable() {
             @Override
             public void run() {
                 while (true) {
                     sell(Thread.currentThread().getName());
                     if (ticket <= 0) {
                         break;
                     }
                 }
             }
         });
     }

        /**
         * 三种方式进行线程的防治
         *   1.synchronized
         *   2.lock锁方式
         * @param name
         */
        private  void sell(String name){
            //lock.lock();
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            if (ticket > 0) {
                System.out.println(name + "卖票:" + ticket);
                ticket--;
            }
            //lock.unlock();
        }
    }
}

3. 线程池在业务中使用

线程池的具体使用可以应用在多个方面:

1.例如对于数据库的数据进行清洗,可以使用多线程进行处理

2.对于业务中多次调用第三方接口,由串行的方式更改为并行方式。

3.读取excel文档中的多个sheet ,可以建立多个线程池进行读取,每个sheet都又一个线程池进行读取,大大的提高处理速度

3.1 使用多线程处理excel表中的多个sheet数据

建立springboot工程

建立多个sheet

用户sheet: 姓名 年龄 性别

讲师sheet: 姓名 性别 所带班级

班级sheet: 班级 所在阶段

3.1.2 添加EasyPoi依赖

https://gitee.com/lemur/easypoi 官方网站

 			<dependency>
            <groupId>cn.afterturn</groupId>
            <artifactId>easypoi-spring-boot-starter</artifactId>
            <version>4.3.0</version>
        </dependency>
        <dependency>
            <groupId>cn.afterturn</groupId>
            <artifactId>easypoi-base</artifactId>
            <version>4.3.0</version>
        </dependency>
        <dependency>
            <groupId>cn.afterturn</groupId>
            <artifactId>easypoi-web</artifactId>
            <version>4.3.0</version>
        </dependency>
        <dependency>
            <groupId>cn.afterturn</groupId>
            <artifactId>easypoi-annotation</artifactId>
            <version>4.3.0</version>
        </dependency>

3.1.3 简单导入


3.1.4 多线程导入

package com.qf.springbooteasypoi;

import cn.afterturn.easypoi.excel.ExcelImportUtil;
import cn.afterturn.easypoi.excel.entity.ImportParams;
import cn.afterturn.easypoi.excel.entity.result.ExcelImportResult;
import cn.afterturn.easypoi.excel.imports.ExcelImportService;
import com.qf.springbooteasypoi.excetor.MyExcetorPoolReadExcel;
import com.qf.springbooteasypoi.pojo.Classes;
import com.qf.springbooteasypoi.pojo.Student;
import com.qf.springbooteasypoi.pojo.Teacher;
import org.apache.poi.ss.usermodel.*;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@SpringBootTest
public class ExcetorPoiTest {


    @Test
    public  void  ImportExcel() throws Exception {
         FileInputStream in = null;
        try {
            in = new FileInputStream("C:\\ceshi.xlsx");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        //设置 读取时的配置信息
        ImportParams params = new ImportParams();
        //设置最大的读取的sheet的数量
        params.setSheetNum(3);
        //不进行自动的分割
        params.setVerifyFileSplit(false);
        //获取到所有的返回对象,进行自己的读取配置
        ExcelImportResult<Object> objectExcelImportResult = ExcelImportUtil.importExcelMore(in, Student.class, params);
        Workbook workbook = objectExcelImportResult.getWorkbook();
        //循环所有的sheet标签页


       for (int sheetIndex=0;sheetIndex<workbook.getNumberOfSheets();sheetIndex++) {
           FileInputStream inputStream = new FileInputStream("C:\\ceshi.xlsx");
           //System.out.println(inputStream);
           ImportParams paramss = new ImportParams();
           paramss.setHeadRows(1);
           paramss.setTitleRows(0);
           paramss.setStartSheetIndex(sheetIndex);
           paramss.setVerifyFileSplit(false);
           final String sheetName = workbook.getSheetName(sheetIndex);
           System.out.println(sheetName);


           if (sheetName.equals("学生")) {

               // paramss.setSheetNum(sheetIndex+1);
               Thread thread = new Thread(new Runnable() {
                   @Override
                   public void run() {
                       try {
                           System.out.println(Thread.currentThread().getName());
                           //list = new ExcelImportService().importExcelByIs(inputStream, Student.class, paramss, false).getList();
                           //获取到单个sheet
                           Sheet workbookSheet = workbook.getSheet("学生");
                           //获取到当前sheet中的排的数量 进行循环获取单个row
                           int lastRowNum = workbookSheet.getLastRowNum();
                           // System.out.println(lastRowNum);
                           //从第一行进行读取,排除掉表头,每一行声明一个新对象,进行填充数据
                           List lists = new ArrayList();
                           for (int rowNum = 1; rowNum < lastRowNum; rowNum++) {
                               Student student = new Student();
                               Row row = workbookSheet.getRow(rowNum);
                               //获取单元格的最大数量
                               short lastCellNum = row.getLastCellNum();
                               for (int cellNum = 0; cellNum < lastCellNum; cellNum++) {
                                   //开始处理单元格的数据
                                   Cell cell = row.getCell(cellNum);
                                   //判断单元格的数据的格式,进行比较
                                   CellType cellType = cell.getCellType();

                                   if (cellType.getCode() == 0 && cellNum == 1) {
                                       student.setAge((int) cell.getNumericCellValue());
                                       System.out.println(cell.getNumericCellValue());
                                   } else if (cellType.getCode() == 1 && cellNum == 0) {
                                       student.setStudentName(cell.getStringCellValue());
                                       System.out.println(cell.getStringCellValue());
                                   } else if (cellNum == 2) {
                                       student.setSex(cell.getStringCellValue());
                                   }
                                   //System.out.println(stringCellValue);
                               }
                               lists.add(student);
                           }
                           System.out.println(Thread.currentThread().getName() + "==" + lists);
                       } catch (Exception e) {
                           e.printStackTrace();
                       }
                   }
               }, "学生线程");
               thread.start();
           }
           if (sheetName.equals("讲师")) {
               Thread thread = new Thread(new Runnable() {
                   @Override
                   public void run() {
                       try {
                           System.out.println(Thread.currentThread().getName());
                           //list = new ExcelImportService().importExcelByIs(inputStream, Student.class, paramss, false).getList();
                           //获取到单个sheet
                           Sheet workbookSheet = workbook.getSheet("讲师");
                           //获取到当前sheet中的排的数量 进行循环获取单个row
                           int lastRowNum = workbookSheet.getLastRowNum();
                           // System.out.println(lastRowNum);
                           //从第一行进行读取,排除掉表头,每一行声明一个新对象,进行填充数据
                           List lists = new ArrayList();
                           for (int rowNum = 1; rowNum < lastRowNum; rowNum++) {
                               Teacher teacher = new Teacher();
                               Row row = workbookSheet.getRow(rowNum);
                               //获取单元格的最大数量
                               short lastCellNum = row.getLastCellNum();
                               for (int cellNum = 0; cellNum < lastCellNum; cellNum++) {
                                   //开始处理单元格的数据
                                   Cell cell = row.getCell(cellNum);
                                   //将数据设置到对象中
                                   if (cellNum == 0) {
                                       teacher.setTeacherName(cell.getStringCellValue());
                                   } else if (cellNum == 1) {
                                       teacher.setSex(cell.getStringCellValue());
                                   } else if (cellNum == 2) {
                                       teacher.setClassName(String.valueOf(cell.getNumericCellValue()));
                                   }
                               }
                               lists.add(teacher);
                           }
                           System.out.println(Thread.currentThread().getName() + "==" + lists);
                       } catch (Exception e) {
                           e.printStackTrace();
                       }
                   }
               }, "讲师线程");
               thread.start();
           }
           if (sheetName.equals("班级")) {
               Thread thread = new Thread(new Runnable() {
                   @Override
                   public void run() {
                       try {
                           System.out.println(Thread.currentThread().getName());
                           //list = new ExcelImportService().importExcelByIs(inputStream, Student.class, paramss, false).getList();
                           //获取到单个sheet
                           Sheet workbookSheet = workbook.getSheet("班级");
                           //获取到当前sheet中的排的数量 进行循环获取单个row
                           int lastRowNum = workbookSheet.getLastRowNum();
                           // System.out.println(lastRowNum);
                           //从第一行进行读取,排除掉表头,每一行声明一个新对象,进行填充数据
                           List lists = new ArrayList();
                           for (int rowNum = 1; rowNum < lastRowNum; rowNum++) {
                               Classes classes = new Classes();
                               Row row = workbookSheet.getRow(rowNum);
                               //获取单元格的最大数量
                               short lastCellNum = row.getLastCellNum();
                               for (int cellNum = 0; cellNum < lastCellNum; cellNum++) {
                                   //开始处理单元格的数据
                                   Cell cell = row.getCell(cellNum);
                                   //将数据设置到对象中
                                   if (cellNum == 0) {
                                       classes.setClassName(String.valueOf(cell.getNumericCellValue()));
                                   } else if (cellNum == 1) {
                                       classes.setStage(cell.getStringCellValue());
                                   }
                               }
                               lists.add(classes);
                           }
                           System.out.println(Thread.currentThread().getName() + "==" + lists);
                       } catch (Exception e) {
                           e.printStackTrace();
                       }
                   }
               }, "班级线程");
               thread.start();

           }


           }

    }
}

3.1.5 线程池导入

package com.qf.springbooteasypoi;

import cn.afterturn.easypoi.excel.ExcelImportUtil;
import cn.afterturn.easypoi.excel.entity.ImportParams;
import cn.afterturn.easypoi.excel.entity.result.ExcelImportResult;
import cn.afterturn.easypoi.excel.imports.ExcelImportService;
import com.qf.springbooteasypoi.excetor.MyExcetorPoolReadExcel;
import com.qf.springbooteasypoi.pojo.Classes;
import com.qf.springbooteasypoi.pojo.Student;
import com.qf.springbooteasypoi.pojo.Teacher;
import org.apache.poi.ss.usermodel.*;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@SpringBootTest
public class ExcetorPoiTest {


    @Test
    public  void  ImportExcel() throws Exception {


         FileInputStream in = null;
        try {
            in = new FileInputStream("C:\\ceshi.xlsx");
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }
        //设置 读取时的配置信息
        ImportParams params = new ImportParams();
        //设置最大的读取的sheet的数量
        params.setSheetNum(3);
        //不进行自动的分割
        params.setVerifyFileSplit(false);
        //获取到所有的返回对象,进行自己的读取配置
        ExcelImportResult<Object> objectExcelImportResult = ExcelImportUtil.importExcelMore(in, Student.class, params);
        Workbook workbook = objectExcelImportResult.getWorkbook();
        //循环所有的sheet标签页
        int nThreads =workbook.getNumberOfSheets() ;
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(51), r -> {
            Thread t = new Thread(r);
            t.setUncaughtExceptionHandler((t1, e) -> System.err.println("线程异常:thread={},异常e={}"+ t1+"=="+ e.getMessage()));
            return t;
        });
       for (int sheetIndex=0;sheetIndex<workbook.getNumberOfSheets();sheetIndex++) {
           FileInputStream inputStream = new FileInputStream("C:\\ceshi.xlsx");
           //System.out.println(inputStream);
           ImportParams paramss = new ImportParams();
           paramss.setHeadRows(1);
           paramss.setTitleRows(0);
           paramss.setStartSheetIndex(sheetIndex);
           paramss.setVerifyFileSplit(false);
           final String sheetName = workbook.getSheetName(sheetIndex);
           if (sheetName.equals("学生")) {
               threadPoolExecutor.execute(new Runnable() {
                   @Override
                   public void run() {
                       try {
                           //list = new ExcelImportService().importExcelByIs(inputStream, Student.class, paramss, false).getList();
                           //获取到单个sheet
                           Sheet workbookSheet = workbook.getSheet("学生");
                           //获取到当前sheet中的排的数量 进行循环获取单个row
                           int lastRowNum = workbookSheet.getLastRowNum();
                           //从第一行进行读取,排除掉表头,每一行声明一个新对象,进行填充数据
                           List lists = new ArrayList();
                           for (int rowNum = 1; rowNum < lastRowNum; rowNum++) {
                               Student student = new Student();
                               Row row = workbookSheet.getRow(rowNum);
                               //获取单元格的最大数量
                               short lastCellNum = row.getLastCellNum();
                               for (int cellNum = 0; cellNum < lastCellNum; cellNum++) {
                                   //开始处理单元格的数据
                                   Cell cell = row.getCell(cellNum);
                                   //判断单元格的数据的格式,进行比较
                                   CellType cellType = cell.getCellType();

                                   if (cellType.getCode() == 0 && cellNum == 1) {
                                       student.setAge((int) cell.getNumericCellValue());
                                   } else if (cellType.getCode() == 1 && cellNum == 0) {
                                       student.setStudentName(cell.getStringCellValue());
                                   } else if (cellNum == 2) {
                                       student.setSex(cell.getStringCellValue());
                                   }
                               }
                               lists.add(student);
                           }
                           System.out.println(Thread.currentThread().getName() + "==" + lists);
                       } catch (Exception e) {
                           e.printStackTrace();
                       }
                   }
               });

            }
           if (sheetName.equals("讲师")) {
               threadPoolExecutor.execute(new Runnable() {
                   @Override
                   public void run() {
                       try {
                           //list = new ExcelImportService().importExcelByIs(inputStream, Student.class, paramss, false).getList();
                           //获取到单个sheet
                           Sheet workbookSheet = workbook.getSheet("讲师");
                           //获取到当前sheet中的排的数量 进行循环获取单个row
                           int lastRowNum = workbookSheet.getLastRowNum();
                           //从第一行进行读取,排除掉表头,每一行声明一个新对象,进行填充数据
                           List lists = new ArrayList();
                           for (int rowNum = 1; rowNum < lastRowNum; rowNum++) {
                               Teacher teacher = new Teacher();
                               Row row = workbookSheet.getRow(rowNum);
                               //获取单元格的最大数量
                               short lastCellNum = row.getLastCellNum();
                               for (int cellNum = 0; cellNum < lastCellNum; cellNum++) {
                                   //开始处理单元格的数据
                                   Cell cell = row.getCell(cellNum);
                                   //将数据设置到对象中
                                   if (cellNum == 0) {
                                       teacher.setTeacherName(cell.getStringCellValue());
                                   } else if (cellNum == 1) {
                                       teacher.setSex(cell.getStringCellValue());
                                   } else if (cellNum == 2) {
                                       teacher.setClassName(String.valueOf(cell.getNumericCellValue()));
                                   }
                               }
                               lists.add(teacher);
                           }
                           System.out.println(Thread.currentThread().getName() + "==" + lists);
                       } catch (Exception e) {
                           e.printStackTrace();
                       }
                   }
               });

           }
           if (sheetName.equals("班级")) {
               threadPoolExecutor.execute(new Runnable() {
                      @Override
                   public void run(){
                          try {
                              //list = new ExcelImportService().importExcelByIs(inputStream, Student.class, paramss, false).getList();
                              //获取到单个sheet
                              Sheet workbookSheet = workbook.getSheet("班级");
                              //获取到当前sheet中的排的数量 进行循环获取单个row
                              int lastRowNum = workbookSheet.getLastRowNum();
                              //从第一行进行读取,排除掉表头,每一行声明一个新对象,进行填充数据
                              List lists = new ArrayList();
                              for (int rowNum = 1; rowNum < lastRowNum; rowNum++) {
                                  Classes classes = new Classes();
                                  Row row = workbookSheet.getRow(rowNum);
                                  //获取单元格的最大数量
                                  short lastCellNum = row.getLastCellNum();
                                  for (int cellNum = 0; cellNum < lastCellNum; cellNum++) {
                                      //开始处理单元格的数据
                                      Cell cell = row.getCell(cellNum);
                                      //将数据设置到对象中
                                      if (cellNum == 0) {
                                          classes.setClassName(String.valueOf(cell.getNumericCellValue()));
                                      } else if (cellNum == 1) {
                                          classes.setStage(cell.getStringCellValue());
                                      }
                                  }
                                  lists.add(classes);
                              }
                              System.out.println(Thread.currentThread().getName() + "==" + lists);
                          } catch (Exception e) {
                              e.printStackTrace();
                          }
                      }
                 });
           }

       }

    }
}

3.2 有多个第三方接口调用时

3.2.1 提供两个第三方接口

短信
package com.qf.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/sms")
@RestController
public class SmsController {


    @RequestMapping("/send")
    public String email() throws InterruptedException {
        Thread.sleep(5000);
        System.out.println("发送短信");
        return "ok";
    }
}

邮件
package com.qf.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RequestMapping("/email")
@RestController
public class EmailController {


    @RequestMapping("/send")
    public String email() throws InterruptedException {
        Thread.sleep(5000);
        System.out.println("发送邮件");
        return "ok";
    }
}

串行方式
 public  static  RestTemplate restTemplate = new RestTemplate();
    @RequestMapping("/registry")
    public String registry(){
        System.out.println("开始时间:"+new Date());

        //发送短信 发送邮箱 分别调用两个服务
        String forObject = restTemplate.getForObject("http://localhost:8081/email/send", String.class);
        String forObject1 = restTemplate.getForObject("http://localhost:8082/sms/send", String.class);
        System.out.println("注册逻辑");
        System.out.println("结束时间:"+new Date());
        return "ok";
    }
并行方式
 @RequestMapping("/registryThread")
    public String registryThread() {
        System.out.println("开始时间:"+new Date());
        RestTemplate restTemplate = new RestTemplate();
        //声明线程池
        List<String> listUrl = new ArrayList<>();
        listUrl.add("http://localhost:8082/sms/send");
        listUrl.add("http://localhost:8081/email/send");
        ThreadPoolExecutor mythread = Mythread();
        //发送短信 发送邮箱 分别调用两个服务
        for (String url:listUrl) {
            mythread.submit(new Runnable() {
                @Override
                public void run() {
                    System.out.println(Thread.currentThread().getName());
                    send(url);
                }
            });
        }
        System.out.println("注册逻辑");
        System.out.println("结束时间:"+new Date());
        return "ok";
    }

    public static ThreadPoolExecutor Mythread(){
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(2, 2, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(51), r -> {
            Thread t = new Thread(r);
            t.setUncaughtExceptionHandler((t1, e) -> System.err.println("线程异常:thread={},异常e={}"+ t1+"=="+ e.getMessage()));
            return t;
        });

    return threadPoolExecutor;
    }

    public static String send(String url){
       return restTemplate.getForObject(url, String.class);

    }

3.3 消费监听时,启动多个线程进行监听

发送端
package com.qf;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

@SpringBootTest
@RunWith(SpringRunner.class)
public class RabbitSendTest {

    @Autowired
    RabbitTemplate rabbitTemplate;

    @Test
    public void testSend(){
        for (int i=0;i<1000;i++){
            rabbitTemplate.convertAndSend("", "thread-test", "测试数据"+i);
        }
    }


}

消费端
package com.qf.listen;

import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.util.Date;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Future;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

@Component
public class RabbitListen {

    private static ThreadPoolExecutor threadPoolExecutor=new ThreadPoolExecutor(10, 12, 0L, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(51), r -> {
        Thread t = new Thread(r);
        t.setUncaughtExceptionHandler((t1, e) -> System.err.println("线程异常:thread={},异常e={}"+ t1+"=="+ e.getMessage()));
        return t;
    });;

    @RabbitListener(queues = "thread-test")
    public void listen(String msg){
        threadPoolExecutor.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+":"+msg);
            }
        });

    }

}

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

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

相关文章

微信小程序开发【从0到1~入门篇】

目录 1. 微信小程序介绍 1.1 什么是小程序&#xff1f; 1.2 小程序可以干什么&#xff1f; 2. 申请账号 2.1 申请帐号 2.2 测试号申请&#xff08;我们小程序账号申请完成之后&#xff0c;建议务必要申请一个测试号用来开发&#xff09; 3. 安装开发工具 3.1 选择稳定…

我的创作纪念日(2021-12-10 2022-12-10)

&#x1f306; 内容速览阴差阳错成为一名博主&#xff1f;这一年来的收获日常生活未来憧憬阴差阳错成为一名博主&#xff1f; 如上图所见&#xff0c;她就是我在CSDN上发布的第一篇博客——无标题&#xff0c;有时候机缘来的那么突然&#xff0c;我甚至都没有给她想一个凑合的名…

spring——Spring 注入内部Bean——构造函数方式注入内部 Bean

项目依赖&#xff1a; <?xml version"1.0" encoding"UTF-8"?> <project xmlns"http://maven.apache.org/POM/4.0.0"xmlns:xsi"http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation"http://maven.apache.or…

【MySQL进阶篇】存储引擎

&#x1f349;个人主页&#xff1a;个人主页 &#x1f353;系列专栏&#xff1a;MySQL数据库 目录 1.MySQL体系结构 1). 连接层 2). 服务层 3). 引擎层 4). 存储层 2.存储引擎介绍 3.存储引擎特点 1. InnoDB 2.MyISAM 3.Memory 4.区别及特点 4.存储引擎选择 1.MySQ…

MAC QT OpenGL 图像曝光度调节

目录 一.MAC QT OpenGL 图像曝光度调节演示 1.原始图片2.效果演示 二.MAC QT OpenGL 图像曝光度调节源码下载三.其他平台图像曝光度调节版本 1.IOS 曝光度演示效果2.Windows OpenGL ES 曝光度演示效果3.Windows OpenGL 曝光度演示效果 四.猜你喜欢 零基础 OpenGL ES 学习路线推…

39-kafka-监控Eagle

39-kafka-监控Eagle&#xff1a; Eagle的安装 1.修改 kafka 启动命令 修改 kafka-server-start.sh 命令中 if [ "x$KAFKA_HEAP_OPTS" "x" ]; then export KAFKA_HEAP_OPTS"-Xmx1G -Xms1G" fi 为 if [ "x$KAFKA_HEAP_OPTS" &qu…

功能测试(八)—— APP之专项测试、性能测试、性能测试工具SoloPi

目录 APP测试要点 目标 一、APP专项测试 1.1 兼容性 1.2 安装 1.3 卸载 1.4 升级 1.5 干扰测试(交叉事件测试) 1.6 Push推送 1.7 用户体验 二、 性能测试工具 2.1 APP性能测试工具介绍 —— SoloPi简介 2.2 APP性能测试工具 —— SoloPi使用 三、APP性能测试 3.…

English Learning - L2 窥得大段表达门径 2022.12.8 周四

English Learning - L2 窥得大段表达门径 2022.12.8 周四引言2 形容词2.1 -ing 形容词 VS -ed 形容词核心思想举例3 名词3.1 修饰成分修饰成分的排列的黄金原则&#xff1a;左二右六举例3.2 名词的数3.2.1 "名词 介词/副词/不定式 等" 构成的复合名词变复数&#xf…

Spring Boot 使用 Micrometer 集成 Prometheus 监控 Java 应用性能

一、背景 SpringBoot的应用监控方案比较多,SpringBoot + Prometheus + Grafana是目前比较常用的方案之一。它们三者之间的关系大概如下图: 二、Micrometer的介绍 Micrometer为Java 平台上的性能数据收集提供了一个通用的 API,它提供了多种度量指标类型(Timers、Guauges、…

【强化学习论文合集】十三.2018机器人与自动化国际会议论文(ICRA2018)

强化学习(Reinforcement Learning, RL),又称再励学习、评价学习或增强学习,是机器学习的范式和方法论之一,用于描述和解决智能体(agent)在与环境的交互过程中通过学习策略以达成回报最大化或实现特定目标的问题。 本专栏整理了近几年国际顶级会议中,涉及强化学习(Rein…

超详细的pytest教程(二)之前后置方法和fixture机制

前言 上一篇文章入门篇咱们介绍了pytest的基本使用&#xff0c;这一篇文章专门给大家讲解pytest中关于用例执行的前后置步骤处理,pytest中用例执行的前后置处理既可以通过测试夹具(fixtrue)来实现&#xff0c;也可以通过xunit 风格的前后置方法来实现。接下来我们一起看看如何…

Unity - 技术美术

198.Shader Graph 旗帜飘扬 官方教程链接&#xff1a;https://learn.unity.com/project/make-a-flag-move-with-shadergraph 本节课程文档&#xff1a;https://gitee.com/chutianshu1981/AwesomeUnityTutorial/blob/main/%E5%9B%BE%E5%BD%A2-%E6%8A%80%E6%9C%AF%E7%BE%8E%E5%B…

ElasticSearch入门到springboot使用

文章目录1.存储引擎产品性能对比2.es安装1.创建目录2.创建挂载的配置文件3.编写docker-compose4.添加文件夹权限5.启动es与kibana6.开放端口7.测试访问8.安装IK分词器3.es核心概念1.文档&#xff1a;就是一条数据2.类型&#xff1a;表字段和类型3.索引&#xff1a;就是数据库4.…

【WIN】Windows10 开启远程连接图形化界面(mstsc)

CONTENTwindows10 rdp 开启远程连接开启远程连接win10 专业版遇到的问题3389 端口不可用简述具体解决windows10 rdp 开启远程连接 开启远程连接 win10 专业版 快捷键&#xff1a; winI 打开设置&#xff0c;然后 #mermaid-svg-bTRFQYmaW8UwxJ5Y {font-family:"trebuche…

java计算机毕业设计ssm医疗垃圾管理系统f5aj8(附源码、数据库)

java计算机毕业设计ssm医疗垃圾管理系统f5aj8&#xff08;附源码、数据库&#xff09; 项目运行 环境配置&#xff1a; Jdk1.8 Tomcat8.5 Mysql HBuilderX&#xff08;Webstorm也行&#xff09; Eclispe&#xff08;IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持&#xff0…

针对低功率/低电源电压应用的5MBd数字光耦合器

针对低功率/低电源电压应用的5MBd数字光耦合器 介绍 电气系统中的数字光电耦合器提供高压绝缘和高压绝缘数据传输时的噪声抑制。一个高质量的绝缘屏障&#xff0c;在里面光耦合器需要提供卓越的可靠性和耐久性信号隔离。 除了绝缘和噪音抑制能力&#xff0c;新的5MBd数字光耦…

Image-Line升级FL Studio21水果DAW音乐工作站

FL Studio水果21加入了更快、更精确的音频编辑&#xff0c;改进了内容搜索&#xff0c;DAW“情绪主题”控制&#xff0c;甚至还有更多的灵感、创意工具。 如果你真正需要中文语言&#xff0c;你需要更新到FL Studio 21版本打开你的FL设置&#xff0c;在通用&#xff08;Genera…

Wireshark | 猿如意

文章目录一、前言二、猿如意介绍客户端网页版三、Wireshark简介发展史使用猿如意下载功能介绍使用方法使用感受一、前言 我曾经有过这样的烦恼&#xff1a;去下载一个应用&#xff0c;结果百度到了一大堆广告&#xff0c;最后下载的也不是官方版本&#xff0c;还捆绑了一大堆AP…

SpringBoot中使用Easyexcel实现Excel导入导出功能(二)

目录 常规导出 大批量数据导出 复杂表头的导出 日期、数字、自定义格式转换后导出 常规导出 常规导出excel有两种&#xff0c;个人比较推荐第一种&#xff1a; 1、新建一个导出数据的实体类&#xff0c;用ExcelProperty()注解标明excel中列的中文名称&#xff1b;如果实体的…

maven学习手册

maven学习手册1.maven简介1.1 传统项目的弊端1.2 Maven是什么2.maven的安装和配置2.1 maven的安装2.2 为阿里云设置代理仓库2.3 maven常用命令简单说明3.maven实战3.1 一个简单的maven项目3.2 maven常用命令简单说明3.3 为这个简单的maven项目安装依赖3.4 maven常见标签说明4.依…