推广公司网站有哪些方式,wordpress常用的插件,做网站要用到哪些技术,体育彩票网站开发方案对于刚刚从校园出来的菜鸡选手很容易写出自以为没问题的屎山代码#xff0c;可是当上线后就会立即暴露出问题#xff0c;这说到底还是基础不够扎实#xff01;只会背八股文#xff0c;却不理解#xff0c;面试头头是道#xff0c;一旦落地就啥也不是。此处#xff0c;抛… 对于刚刚从校园出来的菜鸡选手很容易写出自以为没问题的屎山代码可是当上线后就会立即暴露出问题这说到底还是基础不够扎实只会背八股文却不理解面试头头是道一旦落地就啥也不是。此处抛出几个八股文为什么要使用线程池使用Executors的工厂类创建的线程池会有什么问题阿里为什么推荐使用标准构造器ThreadPoolExecutor创建线程池使用线程池提交任务submit 和 execute 有啥不一样吗等等如果以上的问题你回答不出那么你距离一个中级软件工程师还有很大差距我建议系统性学习一下如果只是想知道答案可以访问我的 私人知识库 Java专项中的多线程与线程池篇章 也可阅读我另外一篇博客Java高并发核心编程(JUC)—线程池详细笔记。本文就以开发中的实际问题作为切入点练习使得以后初入职场能够快速上手问题少少 任务情景
假设你在项目中有个异步操作需要实现例如用户请求生成体检报告假设体检报告生成需要5分钟这时候你可以立即告诉用户体检报告生成中稍后发送至用户的邮箱。此时用户就不用在这儿等待了可以退出系统了。作为菜鸡的我们很可能写出下面的代码
Test
public void threadTaskClassical() throws InterruptedException {// 模拟100个用户请求for (int i 0; i 100; i) {new Thread(()-{System.out.println(任务开始);try {System.out.println(体检报告生成中...);Thread.sleep(20000L);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(将体检报告发送至用户邮箱);}).start();System.out.println(请求成功提交报告生成中稍后发送至您的邮箱); }Thread.sleep(100000L);
}上面代码的for循环是用来模拟用户提交生成提交报告请求的而当用户提交请求将立即得到响应 “请求成功提交报告生成中稍后发送至您的邮箱”。这里为了便于举例使用 Thread.sleep(20000L); 来模拟5分钟生成体检报告。问题来了以上代码看起来没啥问题但是落地后大概率会出现什么问题会导致虚拟机崩溃因为用户的每个请求都将创建一个线程线程的内存空间是在虚拟机栈内存中的每开启一个线程将在虚拟机栈内存中开辟一个栈虚拟机默认一个线程栈内存1MB大小可以通过 -Xss 参数来设置而线程中的每个方法的物理内存大小叫做栈帧存放在栈中。因此以上代码当用户量过大例如同时有1万人使用那么会导致栈内存溢出 每个线程在执行时都有自己的调用栈stack其中保存了方法的调用和局部变量等信息。如果过度创建线程可能导致栈内存溢出因为每个线程的栈内存是有限的。总的来说不使用线程池而直接创建大量线程可能导致资源耗尽、栈内存溢出、调度开销增加等问题从而影响应用程序的稳定性和性能。
使用线程池改进
理解以上理论后很容易使用线程池来改进但是依旧会存在问题的哦我们慢慢来首先使用更加low一点的线程池来改进于是写下如下代码
Test
public void threadPoolTaskClassical() throws InterruptedException {ExecutorService pools Executors.newFixedThreadPool(10);// 模拟100个用户请求for (int i 0; i 100; i) {pools.execute(()-{System.out.println(任务开始);try {System.out.println(体检报告生成中...);Thread.sleep(20000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(将体检报告发送至用户邮箱);});System.out.println(请求成功提交报告生成中稍后发送至您的邮箱);}Thread.sleep(100000L);
}这个是使用固定的线程池即线程池最大只支持同时10个线程同时工作超过的线程将排队等待其他线程执行完毕。这样的话会导致任务不断进入等待队列最终等待队列存放不下新来的任务而导致溢出使得虚拟机OOM崩溃于是Executors工厂类有各种各样的线程池总结如下
newFixedThreadPool(int nThreads) 创建一个固定大小的线程池该线程池中的线程数量始终保持不变。如果某个线程因为执行异常而终止会有新的线程来替代它。newCachedThreadPool() 创建一个根据需要创建新线程的线程池。线程池的大小没有限制。如果线程在60秒内未被使用则将其终止并从池中移除。newSingleThreadExecutor() 创建一个单线程的线程池该线程池中的线程按顺序执行提交的任务。如果这个唯一的线程在执行任务时抛出异常将会创建一个新的线程来替代它。newScheduledThreadPool(int corePoolSize) 创建一个固定大小的线程池支持定时及周期性任务执行。
最后两个方法比较抽象一点下面给出最后一个的代码实现这样就能理解了
ScheduledExecutorService scheduledThreadPool Executors.newScheduledThreadPool(3);
// 在延迟2秒后执行任务
scheduledThreadPool.schedule(() - System.out.println(Delayed task), 2, TimeUnit.SECONDS);
// 在延迟1秒后每隔3秒执行一次任务
scheduledThreadPool.scheduleAtFixedRate(() - System.out.println(Periodic task), 1, 3, TimeUnit.SECONDS);以上只是简单介绍一下详细的内容在我的另外一篇博客中已经讲解的非常清楚了欢迎阅读Java高并发核心编程(JUC)—线程池详细笔记
为什么禁止使用Executors快捷创建线程池
FixedThreadPool和SingleThreadPool 这两个工厂方法所创建的线程池工作队列任务排队的队列长度都为Integer.MAX_VALUE可能会堆积大量的任务从而导致OOM即耗尽内存资源。CachedThreadPool和ScheduledThreadPool 这两个工厂方法所创建的线程池允许创建的线程数量为Integer.MAX_VALUE可能会导致创建大量的线程从而导致OOM问题。
所以大厂的编程规范都不允许使用Executors创建线程池而是要求使用标准构造器ThreadPoolExecutor创建线程池。
最优雅的方式
大部分企业的开发规范都会禁止使用快捷线程池具体原因稍后介绍要求通过标准构造器ThreadPoolExecutor去构造工作线程池。 Executors工厂类中创建线程池的快捷工厂方法实际上是调用ThreadPoolExecutor 定时任务使用ScheduledThreadPoolExecutor 线程池的构造方法完成的。ThreadPoolExecutor构造方法
public ThreadPoolExecutor(int corePoolSize,// 核心线程数即使线程空闲Idle 也不会回收int maximumPoolSize,// 线程数的上限long keepAliveTime,// 线程最大空闲Idle时长TimeUnit unit, // 时间单位BlockingQueueRunnable workQueue, //任务的阻塞排队队列ThreadFactory threadFactory, //新线程的产生方式RejectedExecutionHandler handler //拒绝策略
)关键参数介绍 corePoolSize核心线程数定义了最小可以同时运行的线程数量当在线程池接收到的新任务并且当前工作线程数少于corePoolSize时即使其他工作线程处于空闲状态也会创建一个新线程来处理该请求直到线程数达到corePoolSize。如果当前工作线程数多于corePoolSize数量但小于maximumPoolSize数量那么仅当任务排队队列已满时才会创建新线程。通过设置corePoolSize和maximumPoolSize相同可以创建一个固定大小的线程池。 maximumPoolSize最大线程数当队列中存放的任务达到队列容量时当前可以同时运行的数量变为最大线程数创建线程并立即执行最新的任务与核心线程数之间的差值又叫救急线程数救急线程是有空闲时长的 keepAliveTime当达到最大空闲时长被回收。 keepAliveTime救急线程最大存活时间当线程池中的线程数量大于corePoolSize的时候如果这时没有新的任务提交核心线程外的线程不会立即销毁而是会等到keepAliveTime时间超过销毁。但是如果调用了allowCoreThreadTimeOut(boolean)方法并且传入了参数true则keepAliveTime参数所设置的Idle超时策略也将被应用于核心线程。 workQueue阻塞队列存放被提交但尚未被执行的任务 handler拒绝策略线程到达最大线程数仍有新任务时会执行拒绝策略
线程池的任务调度流程
如果当前工作线程数量小于核心线程池数量执行器总是优先创建一个任务线程而不是从线程队列中获取一个空闲线程。如果线程池中总的任务数量大于核心线程池数量新接收的任务将被加入到阻塞队列中一直到阻塞队列已满。在核心线程池数量已经用完、阻塞队列没有满的场景下线程池不会为新任务创建一个新线程。当完成一个任务的执行时执行器总是优先从阻塞队列中获取下一个任务并开始执行一直到阻塞队列为空其中所有的缓存任务被取光。在核心线程池数量已经用完、阻塞队列也已经满了的场景下如果线程池接收到新的任务将会为新任务创建一个线程非核心线程并且立即开始执行新任务。在核心线程都用完、阻塞队列已满的情况下一直会创建新线程去执行新任务直到池内的线程总数超出maximumPoolSize。如果线程池的线程总数超过maximumPoolSize线程池就会拒绝接收任务当新任务过来时会为新任务执行拒绝策略。 因此最优雅的方式如下你可以修改各种参数以满足你的要求 class MyTask implements Runnable{Overridepublic void run() {System.out.println(任务开始);try {System.out.println(体检报告生成中...);Thread.sleep(20000);} catch (InterruptedException e) {throw new RuntimeException(e);}System.out.println(将体检报告发送至用户邮箱);}}Testpublic void ThreadPoolExecutorDemo() throws InterruptedException {// 创建核心线程为10最大线程为100救急线程存活时间为60秒有界阻塞队列容量为100的线程池ThreadPoolExecutor threadPoolExecutor new ThreadPoolExecutor(10,100,60,TimeUnit.SECONDS,new LinkedBlockingDequeRunnable(100));for (int i 0; i 100; i) {threadPoolExecutor.execute(new MyTask());
// threadPoolExecutor.submit(new MyTask());System.out.println(请求成功提交报告生成中稍后发送至您的邮箱);}Thread.sleep(100000L);}最后提一嘴execute 和 submit 提交任务有啥不同如果都没有返回值那么两个方法没啥区别如果需要获取异步任务的执行结果就用 submit 并且submit既可以提交Runnable没返回值的任务也可以提交Callable有返回值的任务。具体区别如下
execute(Runnable command) 方法是 Executor 接口定义的它用于提交不需要返回结果的任务。execute 方法没有返回值。submit(Runnable task) 方法是 ExecutorService 接口定义的它也用于提交不需要返回结果的任务但与 execute 不同submit 方法返回一个 Future 对象。execute 方法适用于 Runnable 类型的任务。submit 方法不仅适用于 Runnable 类型的任务还适用于 Callable 类型的任务即返回结果的任务。execute 方法无法获取任务执行的结果或异常。如果任务抛出异常调用者无法捕获到。submit 方法返回一个 Future 对象通过 Future 对象可以获取任务执行的结果同时也能捕获到任务抛出的异常。