首页 > 编程语言 >Java面试高频技术线程池,源码笔记答案全纪录

Java面试高频技术线程池,源码笔记答案全纪录

时间:2023-07-12 21:31:55浏览次数:39  
标签:全纪录 Java 队列 创建 worker 任务 源码 线程 执行


有一定的java基础 (线程) ,尤其是正要或正准备找工作的童鞋

如果想在众多面试者中脱颖而出,你就需要多准备一些知识点,多刷一些面试题。 而对于企业而言,有这么多的选择 那我们就提高面试门槛,可能我需要的仅仅是CRUD的初中级,但我也希望你能了解 JVM、多线程、Spring源码、Sql优化、分布式架构等等,也是我们经常说的 面试造火箭 ,工作拧螺丝

公开课内容:

  • 为什么要使用多线程
  • 线程的生命周期
  • 为什么要使用线程池
  • 线程池的工作原理
  • 线程池的核心参数解读
  • 线程池的源码分析
  • 线程池的实战使用

为什么要使用多线程

进程与线程

进程:

是执行中一段程序,即一旦程序被载入到内存中并准备执行,它就是一个进程。进程是表示资源分配的的基本概念,又是调度运行的基本单位,是系统中的并发执行的单位。

线程:

单个进程中执行中每个任务就是一个线程。线程是进程中执行运算的最小单位。

一个线程只能属于一个进程,但是一个进程可以拥有多个线程。

多线程处理就是允许一个进程中在同一时刻执行多个任务。

线程是一种轻量级的进程,与进程相比,线程给操作系统带来侧创建、维护、和管理的负担要轻,意味着线程的代价或开销比较小

实现程序异步处理

使用线程可以把占据时间长的程序中的任务放到后台去处理,程序的运行速度可能加快,在一些等待的任务实现上如用户输入、文件读写和网络收发数据等,线程就比较有用了。在这种情况下可以释放一些珍贵的资源如内存占用等等。

在web开发中, 经常会有一些操作 需要触发另一个操作, 而对触发操作的结果却不关心 。 如: 记录日志、计算、发通知如果使用同步的方式处理 性能低下 且 该操作报错会影响本身的方法

Java面试高频技术线程池,源码笔记答案全纪录_线程

可以启动一个新线程,异步处理

Java面试高频技术线程池,源码笔记答案全纪录_rxjava_02

利用CPU多核优势并行处理任务

如: 在定时任务中,数据批处理中,文件分片处理中

Java面试高频技术线程池,源码笔记答案全纪录_线程_03

为什么要使用线程池

多线程的情况下确实可以最大限度发挥多核处理器的计算能力,提高系统的吞吐量和性能。但是如果随意使用线程,对系统的性能反而有不利影响。

线程生命周期

线程的创建 和 销毁都需要消耗系统资源

Java面试高频技术线程池,源码笔记答案全纪录_面试_04

线程调度机制

CPU性能有限,当产生大量线程时,需要频繁根据时间片切换线程,而切换线程也会浪费大量系统性能

Java面试高频技术线程池,源码笔记答案全纪录_线程池_05

最终的结果,通过多线程不但没提升效率,反而让程序的效率变低。 甚至会因为大量线程的创建,造成系统内存不足而产生OOM

线程数量选择

过小会导致程序不能充分地利用系统资源、容易导致饥饿

过大会导致更多的线程上下文切换,占用更多内存

CPU 密集型运算

通常采用 cpu 核数 + 1 能够实现最优的 CPU 利用率,+1 是保证当线程由于页缺失故障(操作系统)或其它原因

导致暂停时,额外的这个线程就能顶上去,保证 CPU 时钟周期不被浪费

I/O 密集型运算

CPU 不总是处于繁忙状态,例如,当你执行业务计算时,这时候会使用 CPU 资源,但当你执行 I/O 操作时、远程

RPC 调用时,包括进行数据库操作时,这时候 CPU 就闲下来了,你可以利用多线程提高它的利用率。

经验公式如下

线程数 = 核数 * 期望 CPU 利用率 * 总时间(CPU计算时间+等待时间) / CPU 计算时间

例如 4 核 CPU 计算时间是 50% ,其它等待时间是 50%,期望 cpu 被 100% 利用,套用公式
4 * 100% * 100% / 50% = 8

例如 4 核 CPU 计算时间是 10% ,其它等待时间是 90%,期望 cpu 被 100% 利用,套用公式
4 * 100% * 100% / 10% = 40

线程池介绍

线程池是java在jdk1.5开始提供的一套框架,是为突然大量爆发的线程设计的,可以通过有限的几个固定线程为大量的操作服务减少了创建和销毁线程所需的时间,从而提高效率

Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。

在开发过程中,合理地使用线程池能够带来3个好处。

第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

第三:提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,
还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。

线程池API类图

Java面试高频技术线程池,源码笔记答案全纪录_java_06

关键类或接口

含义

Executor

是一个接口,它是Executor框架的基础,

它将任务的提交与任务的执行分离开来

ExecutorService

线程池的主要接口,是Executor的子接口

ThreadPoolExecutor

是线程池的核心实现类,用来执行被提交的任务

ScheduledThreadPoolExecutor

另一个关键实现类,可以进行延迟或者定期执行任务。ScheduledThreadPoolExecutor比Timer定时器更灵活,

功能更强大

Future接口与FutureTask实现类

代表异步计算的结果

Runnable接口和Callable接口的实现类

都可以被ThreadPoolExecutor或

ScheduledThreadPoolExecutor执行的任务

Executors

线程池的工具类,可以快捷的创建线程池

但是,要做到合理利用线程池,必须对其实现原理了如指掌。

线程池的工作原理

Java面试高频技术线程池,源码笔记答案全纪录_线程_07

任务的执行流程

提交一个任务到线程池中,线程池的处理流程如下:

流程1 判断核心线程数
判断正在运行的工作线程是否小于 设置的核心线程数,小于尝试创建一个
新的工作线程,如果不小于进入下一流程

流程2 判断任务队列
判断当前线程池的任务队列是否已满,未满的话将任务加入任务队列,如果满了,进入下一个流程

流程3 判断最大线程数
判断当前线程池的工作线程是否小于 设置的最大线程数,小于尝试创建一个新的临时工作线程,如果不小于进入下一流程


流程4 判断 饱和/拒绝 策略			
到此流程,说明当前线程池已经饱和,需要进行拒绝策略,根据设置的拒绝策略进行处理

线程池中线程的复用

在线程池中,线程被创建后执行完任务后  不会立刻销毁

而是线程的任务本身就是一个while循环  通过getTask()方法  从workQueue 任务队列中调用要执行的任务, 

获取到任务后, 会执行任务的run方法, 这样实现了任务的复用

线程池中线程的销毁

线程池中线程停止有下面几种情况:
1. 线程池中核心线程默认永远不会销毁 (可以通过allowCoreThreadTimeOut设置允许超时),
   而临时线程  如果空闲时间达到 keepAlivedTime + TimeUnit的值后 会被销毁

2. 如果调用线程池的 shutDown方法, 线程池会在workQueue中的任务执行完毕后 销毁所有线程,关闭线程池

3. 如果调用线程池的 shutDownNow方法, 线程池会立刻终止 , workQueue中的未完成任务 会作为返回值返回

线程池的饱和策略

如果线程池 中工作线程数量已经达到最大线程,并且任务队列已满,说明线程池已经达到饱和状态

会执行参数中传入的拒绝策略 (实现 RejectedExecutionHandler接口 )

ThreadPoolExecutor中 内置了4种拒绝策略:
	
	CallerRunsPolicy: 不丢弃任务,让线程池的调用者线程 参与执行任务
	 
	AbortPolicy: 丢弃后续的任务,并抛出异常
	
	DiscardOldestPolicy: 丢弃任务队列中 存放最久的任务,不抛异常
	
	DiscardPolicy: 丢弃后续任务,不抛异常

线程池的核心参数

Executor框架的最核心实现是ThreadPoolExecutor类,通过传入不同的参数,就可以构造出适用于不同应用场景下的线程池,那么它的底层原理是怎样实现的呢,下面就来介绍下ThreadPoolExecutor线程池的运行过程。

核心构造器参数

组件

含义

int corePoolSize

核心线程池的大小

int maximumPoolSize

最大线程池的大小

BlockingQueue workQueue

用来暂时保存任务的工作队列

RejectedExecutionHandler

当ThreadPoolExecutor已经关闭或ThreadPoolExecutor已经饱和时(达到了最大线程池的大小且工作队列已满),execute()方法将要调用的Handler

long keepAliveTime,

表示空闲线程的存活时间。

TimeUnit

表示keepAliveTime的单位。

ThreadFactory threadFactory

指定创建线程的线程工厂

线程池的三种队列

1.SynchronousQueue

SynchronousQueue没有容量,是无缓冲等待队列,是一个不存储元素的阻塞队列,会直接将任务交给消费者,必须等队列中的添加元素被消费后才能继续添加新的元素。
使用SynchronousQueue阻塞队列一般要求maximumPoolSizes为无界,避免线程拒绝执行操作。

2.LinkedBlockingQueue

LinkedBlockingQueue是一个无界缓存等待队列。当前执行的线程数量达到corePoolSize的数量时,剩余的元素会在阻塞队列里等待。(所以在使用此阻塞队列时maximumPoolSizes就相当于无效了),每个线程完全独立于其他线程。生产者和消费者使用独立的锁来控制数据的同步,即在高并发的情况下可以并行操作队列中的数据。

3.ArrayBlockingQueue

ArrayBlockingQueue是一个有界缓存等待队列,可以指定缓存队列的大小,当正在执行的线程数等于corePoolSize时,多余的元素缓存在ArrayBlockingQueue队列中等待有空闲的线程时继续执行,当ArrayBlockingQueue已满时,加入ArrayBlockingQueue失败,会开启新的线程去执行,当线程数已经达到最大的maximumPoolSizes时,再有新的元素尝试加入ArrayBlockingQueue时会报错。

线程池工具类

Executors是线程池的工具类,提供了四种快捷创建线程池的方法:


newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
newFixedThreadPool 
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
newSingleThreadExecutor 
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

newCachedThreadPool

/**
     * 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
     */
    public void fun1(){
        // 创建线程池
        ExecutorService es = Executors.newCachedThreadPool();
        // 会创建出10个线程   分别执行任务
        for (int i = 0; i < 10; i++) {
            es.execute(()->{
                for (int j = 0; j < 10; j++) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":" + j);
                }
            });
        }
        es.shutdown();
    }

newFixedThreadPool

/**
     * 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
     */
    public void fun2(){
        // 创建线程池
        ExecutorService es = Executors.newFixedThreadPool(2);
        // 会创建出10个线程   分别执行任务
        for (int i = 0; i < 10; i++) {
            es.execute(()->{
                try {
                    Thread.sleep(400);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                for (int j = 0; j < 10; j++) {
                    System.out.println(Thread.currentThread().getName() + ":" + j);
                }
            });
        }
        es.shutdown();
    }

newSingleThreadExecutor

/**
     * 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行
     */ 
public void fun3(){
        // 创建线程池
        ExecutorService es = Executors.newSingleThreadExecutor();
        // 会创建出10个线程   分别执行任务
        for (int i = 0; i < 10; i++) {
            es.execute(()->{
                try {
                    Thread.sleep(400);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                for (int j = 0; j < 10; j++) {
                    System.out.println(Thread.currentThread().getName() + ":" + j);
                }
            });
        }
        es.shutdown();
    }

newScheduledThreadPool

/**
     * 创建一个定长线程池,支持延迟及周期性任务执行。延迟执行示例代码如下
     */
    public void fun4(){
        ScheduledExecutorService newScheduledThreadPool = Executors.newScheduledThreadPool(5);
        // 周期性执行任务(任务会执行多次)
        // 参数1:任务   参数2:延迟时间   参数3:每隔长时间   参数4:时间单位
        newScheduledThreadPool.scheduleAtFixedRate(()-> System.out.println("要执行的任务"), 3, 2, TimeUnit.SECONDS);
        // 延迟执行任务(任务执行一次)
        // 参数1:任务   参数2:延迟时间   参数3:时间单位
        newScheduledThreadPool.schedule(()->System.out.println("要执行的任务"), 3,TimeUnit.SECONDS);
    }
注意:
scheduleAtFixedRate ,是以上一个任务开始的时间计时,period时间过去后,检测上一个任务是否执行完毕,如果上一个任务执行完毕,则当前任务立即执行,如果上一个任务没有执行完毕,则需要等上一个任务执行完毕后立即执行。

scheduleWithFixedDelay,是以上一个任务结束时开始计时,period时间过去后,立即执行。

线程池源码解析

ThreadPoolExecutor源码分析

在线程池的实现中,Worker这个类是线程池的内部类,Worker对象是线程池实现的核心。在ThreadPoolExecutor中存放了一个

// 工作线程的集合
HashSet<Worker> workers = new HashSet<Worker>();

点进Worker类的源码中 发现Worker实现了Runnable接口,并且有两个属性一个线程对象,还有一个第一次要执行的任务

private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable

{
		// 执行工作的线程对象
        final Thread thread;
		// Worker要执行的第一个任务
        Runnable firstTask;
}

查看Worker的构造方法

Worker(Runnable firstTask) {
            setState(-1); 
    		// 传入worker第一次要执行的任务
            this.firstTask = firstTask;
    		// 使用工厂对象创建线程, 并把worker本身传入
            this.thread = getThreadFactory().newThread(this);
}

查看线程工厂对象的newThread类方法

public Thread newThread(Runnable r) {
    // new 了一个新的线程对象  并且把worker对象作为线程任务传入
            Thread t = new Thread(group, r,
                                  namePrefix + threadNumber.getAndIncrement(),
                                  0);
            return t;
}

查看Worker类当中的run方法:

// 线程任务 run方法
public void run() {
   runWorker(this);
}
// Worker的核心工作方法
final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
    	// 第一次要执行的任务 赋值给task
        Runnable task = w.firstTask;
       ...
        try {
            while (task != null || (task = getTask()) != null) {
                w.lock();
					... 
                        // 执行任务的run方法
                        task.run();
                    ...
                } finally {
                	// 任务执行完毕后,清空任务判断是否包含下一个任务
                    task = null;
                    w.completedTasks++;
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
            processWorkerExit(w, completedAbruptly);
        }
    }

查看getTask方法如何获取任务:

private Runnable getTask() {
        boolean timedOut = false; 
        for (;;) {
            ...
            try {
                Runnable r = timed ?
                   // 下面两种方法都是在从 workQueue队列中获取任务
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                // 将取到的任务返回
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }
小结:
通过上面的源码分析得出,Worker对象是线程池工作的核心,一个Worker对象代表一个工作线程, 只要Worker内的线程的start方法被调用后,我们的worker对象内的run方法被线程执行,而run方法中则不断的从任务队列中获取 任务,并调用任务的run方法来执行,这样就达到了线程复用的目的。  

那么Worker什么时候会被创建呢? 

接着分析线程池的execute执行任务的方法

execute的执行源码

// execute执行方法源码分析
		public void execute(Runnable command) {
			// 任务为空抛异常
			if (command == null)
				throw new NullPointerException();
			// ctl 是 integer原子类  主要通过它记录两类信息,
			// ctl作用: 1.记录线程池状态    2.记录线程池工作线程数量 
			// workerCountOf(c):判断worker数量 
			// isRunning(c): 判断线程池状态
			// 1.如果当前worker数量小于corePoolSize  创建一个新的worker
			int c = ctl.get();
			if (workerCountOf(c) < corePoolSize) {
                // 创建worker 参数1:任务 参数2:添加核心还是临时线程
				if (addWorker(command, true))
					return;
				c = ctl.get();
			}
			// 2.尝试向任务队列中添加任务,如果添加失败进入下移流程
			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);
			}
			// 3.参数2:true => 添加核心工作线程   false => 添加临时工作线程   
			else if (!addWorker(command, false))
				
				// 4. 如果添加失败,执行拒绝策略
				reject(command);
		}
总结:通过execute方法的源码,我们就已经看到了执行流程

在查看addWorker的方法,看看是如何添加一个worker的

private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (;;) {
				...
         // wc是工作线程的数量
         // core为true 判断是否大于核心线程数量
         // core为false 判断是否大于最大线程数量
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
                ...
            }
        }
		// 准备创建worker
        Worker w = null;
        try {
            // 创建worker对象,构造器内会通过线程工厂创建一个线程  并且把worker对象作为任务传入
            w = new Worker(firstTask);
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                   ...
                   // 将worker对象 存入到workers集合
                   workers.add(w);
                   int s = workers.size();
                   if (s > largestPoolSize)
                      largestPoolSize = s;
                      workerAdded = true;
                }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                    // 启动worker内的线程
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }
小结:
可以看到,在execute方法中完全和我们前面将的流程一样,在addWorker方法中通过构造器创建了worker对象  并把它存入到了workers集合中,然后启动worker内线程的start方法,这样这个worker就会不断工作,不断的执行任务队列里面的任务

线程池实战场景

异步处理

在头条项目中, 热点评论的计算 和 搜索历史的记录需要异步执行, 使用spring提供的 @EnableAsync @Async使用线程池

Java面试高频技术线程池,源码笔记答案全纪录_线程池_08

并行请求

在电商项目的商品详情功能,要显示的数据分类非常多,需要从不同的微服务中获取, 如果直接的挨个调用Feign方法查询,使用串行方案效率很低,下面的商品详情是通过定义Callable任务, 在通过线程池异步的执行查询,最终汇总结果

Java面试高频技术线程池,源码笔记答案全纪录_面试_09

并行处理

某公司每隔一个周期,会生成所有合作供应商的结算单,在定时任务中,查询所有供应商信息,因为每个供应商都需要统计相关数据,采用串行方案效率低,可以使用线程池 ,通过多个线程并行处理任务

Java面试高频技术线程池,源码笔记答案全纪录_rxjava_10

:
可以看到,在execute方法中完全和我们前面将的流程一样,在addWorker方法中通过构造器创建了worker对象 并把它存入到了workers集合中,然后启动worker内线程的start方法,这样这个worker就会不断工作,不断的执行任务队列里面的任务

## 线程池实战场景

### 异步处理

在头条项目中,  热点评论的计算  和 搜索历史的记录需要异步执行, 使用spring提供的 @EnableAsync    @Async使用线程池



### 并行请求

在电商项目的商品详情功能,要显示的数据分类非常多,需要从不同的微服务中获取,  如果直接的挨个调用Feign方法查询,使用串行方案效率很低,下面的商品详情是通过定义**Callable**任务, 在通过**线程池**异步的执行查询,最终汇总结果




### 并行处理



某公司每隔一个周期,会生成所有合作供应商的结算单,在定时任务中,查询所有供应商信息,因为每个供应商都需要统计相关数据,采用串行方案效率低,可以使用线程池 ,通过多个线程并行处理任务


标签:全纪录,Java,队列,创建,worker,任务,源码,线程,执行
From: https://blog.51cto.com/u_8238263/6704635

相关文章

  • Java TreeMap 介绍与使用
    介绍TreeMap是Java集合框架中的一个类,它实现了SortedMap接口,可以存储键值对,并按照键的自然顺序或者指定的比较器进行排序。TreeMap的底层是一棵红黑树,这是一种自平衡的二叉搜索树,可以保证在插入,删除,查找等操作中的时间复杂度为O(logn)。使用要使用TreeMap,我们需要导入......
  • 你知道Java是世界第一的秘密吗?
    说Java你会说他就是一个计算机语言吧,对它并不是很了解。看完下面的文字,你肯定就不会说你对Java不了解了。Java从1995年诞生到现在已经21年了,他的辉煌你知道吗?Java一直在改变你的生活!傲居语言排行榜榜首Java在TIOBE上的位置TIOBE编程语言社区排行榜是编程语言流行趋势的一个指标,每......
  • JAVA 数字类型 的使用和选择
    JAVA语言中有八种基本的数字类型,分别是byte、short、int、long、float、double、char和boolean。这些类型的区别在于它们所占用的内存空间和表示的范围不同。在使用和选择数字类型时,需要考虑以下几个因素:数字的大小:如果数字很小,可以使用byte或short类型,它们占用1个字......
  • JMeter脚本报错:Cannot find engine named: 'javascript'的解决方法
    本文将介绍如何解决在JMeter版本5.4.1下执行脚本时出现的错误信息“javax.script.ScriptException:Cannotfindenginenamed:'javascript'”。通过将本地JDK版本从18.0.1.1更改为1.8.0_151来解决此问题。当使用JMeter进行脚本执行时,有时可能会遇到以下错误信息:javax.script......
  • Java 基础 - 异常随笔
     异常基础总结try、catch和finally都不能单独使用,只能是try-catch、try-finally或者try-catch-finally。try语句块监控代码,出现异常就停止执行下面的代码,然后将异常移交给catch语句块来处理。catch–用于捕获异常。catch用来捕获try语句块中发生的异常。finally语句块中的......
  • Java字符串逆序的四种方法及比较
    Java中实现字符串逆序有以下几种常见的方法:方法一:使用StringBuffer或StringBuilder的reverse()方法。这是最简单和最直接的方法,只需要将String对象转换为StringBuffer或StringBuilder对象,然后调用它们的reverse()方法,就可以得到逆序的字符串。例如:publicclassStringReverse......
  • 不确定大小的数组怎么办?Java中三种常用的方法
    Java中如何操作不确定大小的数组1. 前言 1.1 什么是数组数组是一种存储多个相同类型数据的有序集合,它可以通过索引来访问每个元素。数组是一种引用类型的变量,它在内存中占用一块连续的空间。 1.2  数组的特点数组有以下几个特点:-数组的长度是确定的,一旦创建就不能......
  • 树莓派人脸识别系统-计算机毕业设计源码+LW文档
    中文摘要计算机技术的发展推动了经济的发展,如今几乎所有的企业都离不开计算机软件,物业单位更是如此。在信息技术不断完善下,物业单位作为人们日常生活不可或缺的组成部分,发挥着重要的作用。然而,随着小区人员的增加,小区门禁管理繁琐,效率低下、进出等待时间长、满意度不高,阻碍了小区......
  • Java实现浏览器端大文件分片上传解决方案
    ​ 上周遇到这样一个问题,客户上传高清视频(1G以上)的时候上传失败。一开始以为是session过期或者文件大小受系统限制,导致的错误。查看了系统的配置文件没有看到文件大小限制,web.xml中seesiontimeout是30,我把它改成了120。但还是不行,有时候10分钟就崩了。同事说,可能是客户这里......
  • Java Map 通过key过滤
    pom文件:<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>31.1-jre</version></dependency>代码:packagecom.example.core.utils.collections;importcom.google.common.......