java不同线程解读以及线程池的使用方式
作者:200.OK
java不同线程解读以及线程池的使用
线程池子类比一个水池,而每一个线程,都好比水池的水。因此,水池多大,看系统硬件配置。线程池主要为了并发,高效处理任务。而异步处理任务,可以有效提高处理任务的吞吐量。
线程池的常见应用场景
处理大量而短小的请求,请求数量很大,每一个请求开启一个线程,在请求完毕之后再对线程进行销毁,这样创建和销毁线程所消耗的时间往往比任务本身所需消耗的资源要大得多。
线程池做到线程复用,不需要频繁的创建和销毁线程,线程池中的线程一直存在于线程池中,线程从任务队列中取得任务来执行。而且这样做的另一个好处有,通过适当地调整线程池中的线程数目,也就是当请求的数目超过某个阈值时,就强制其它任何新到的请求一直等待,直到获得一个线程来处理为止,从而可以防止资源不足。
线程池是什么?
线程池用于多线程处理中,它可以根据系统的情况,可以控制线程执行的数量,优化运行效果。线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。
线程池的作用
创建对象和销毁对象是非常消耗时间和资源的。因此想要最小化这种消耗的一种思想就是『池化资源』,通过重用线程池中的资源来减少创建和销毁线程所需要耗费的时间和资源。
线程池的一个作用是创建和销毁线程的次数,每个工作线程可以多次使用;另一个作用是可根据系统情况调整执行的线程数量,防止消耗过多内存。另外,通过线程池,能有效的控制线程的最大并发数,提高系统资源利用率,同时避免过多的资源竞争,避免堵塞。
线程池的优点总结如下几个方面:
- 线程复用
- 控制最大并发数
- 管理线程
线程池组成:
- 线程池管理器:用于创建并管理线程池
- 工作线程:线程池中的线程
- 任务接口:每个任务必须实现的接口,用于工作线程调度其运行
- 任务队列:用于存放待处理的任务,提供一种缓冲机制
1、Async 和 @Async 很关键
两个注解很关键:
- @EnableAsync 启用基于异步方法的执行
- @Async 这注解的函数会被异步处理
2、Thread和Runnable
Thread可以创建线程,但是线程使用完之后需要对线程资源进行销毁回收,消耗资源,且容易造成线程上下文切换(操作系统核心对CPU上对进程或者线程进行切换)问题,线程管理不当容易资源耗尽,不建议使用。、
一个类实现 Runnable 接口可以将该类的实例传递给一个 Thread 对象并启动一个新线程,这个新线程就会执行该类中 void run() 方法中所定义的逻辑。
3、数据类型—线程安全
如果有多线程共享数据的方式,要牢记各种使用场景的线程安全数据类型。
- (1)、AtomicInteger 原子int整型;
- (2)、AtomicLong 原子long整型;
- (3)、AtomicBoolean 原子boolean;
- (4)、List 这三种都是线程安全型:
①List<T> vector = new Vector<>(); ②<T> listSyn = Collections.synchronizedList(new ArrayList<>()); ③List<T> copyList = new CopyOnWriteArrayList<>();
4、@Configuration配置类和 @Bean将实例对象交给IOC容器
5、ThreadPoolExecutor 和 ThreadPoolTaskExecutor
两种线程池方式,本质上一样,ThreadPoolTaskExecutor(我使用的) 源码上是在 ThreadPoolExecutor 上再加了一层包装,为了更方便在spring框架中使用。
- 配置类: 可以确保异步执行配置对应用中的所有 bean 都生效。
- 启动类: 启动类上整个应用中启用异步
- 推荐: 针对应用中的不同部分提供不同的异步执行策略,或者只需要特定的一部分 bean 具备异步执行能力,放配置类上。如果整个应用都需要异步支持,放置在启动类。
使用@Bean(“beanName”)定义线程池 然后在@Async(“beanName”)中引用指定的线程池
@EnableAsync
用于启用整个应用程序的异步处理功能,包括所有通过 @Async
注解标记的方法。它不负责配置底层线程池。
ThreadPoolTaskExecutor
Bean 配置则是用于配置具体的线程池实例,这个线程池会被 @Async
方法所使用。
如果你在配置类中同时使用了 @EnableAsync
注解和自定义的 ThreadPoolTaskExecutor
Bean 配置,Spring 将会使用你配置的线程池执行异步方法。如果你只使用 @EnableAsync
,Spring 将会使用默认的线程池配置。
注意:
- 1、除了要在方法上加@Async注解,还需要在启动类加注解@EnableAsync启动多线程注解,@Async就会对标注的方法开启异步多线程调用,注意,这个方法的类一定要交给Spring容器来管理。
- 2、法一定要从另一个类中调用,也就是从类的外部调用,类的内部调用是无效的,因为@Transactional和@Async注解的实现都是基于Spring的AOP,而AOP的实现是基于动态代理模式实现的。那么注解失效的原因就很明显了,有可能因为调用方法的是对象本身而不是代理对象,因为没有经过Spring容器
- 3、异步方法使用注解@Async的返回值只能为void或者Future
- 4、方法必须是public方法
定义线程池的配置
package com.test.wll.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; import java.util.concurrent.ThreadPoolExecutor; @EnableAsync//或者加在app类上,此处不加需要在启动类上加 @Configuration public class ThreadPoolTaskConfig { @Bean("threadPoolRedisTaskExecutor") public ThreadPoolTaskExecutor threadPoolRedisTaskExecutor() { ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); //线程池创建的核心线程数,线程池维护线程的最少数量,即使没有任务需要执行,也会一直存活 executor.setCorePoolSize(8); //如果设置allowCoreThreadTimeout=true(默认false)时,核心线程会超时关闭 //executor.setAllowCoreThreadTimeOut(true); //阻塞队列 当核心线程数达到最大时,新任务会放在队列中排队等待执行 executor.setQueueCapacity(124); //最大线程池数量,当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任 //任务队列已满时, 且当线程数=maxPoolSize,,线程池会拒绝处理任务而抛出异常 executor.setMaxPoolSize(64); //当线程空闲时间达到keepAliveTime时,线程会退出,直到线程数量=corePoolSize //允许线程空闲时间30秒,当maxPoolSize的线程在空闲时间到达的时候销毁 //如果allowCoreThreadTimeout=true,则会直到线程数量=0 executor.setKeepAliveSeconds(30); //spring 提供的 ThreadPoolTaskExecutor 线程池,是有setThreadNamePrefix() 方法的。 //jdk 提供的ThreadPoolExecutor 线程池是没有 setThreadNamePrefix() 方法的 executor.setThreadNamePrefix("threadPoolRedisTaskExecutor"); // rejection-policy:拒绝策略:当线程数已经达到maxSize的时候,如何处理新任务 // CallerRunsPolicy():交由调用方线程运行,比如 main 线程;如果添加到线程池失败,那么主线程会自己去执行该任务,不会等待线程池中的线程去执行 // AbortPolicy():该策略是线程池的默认策略,如果线程池队列满了丢掉这个任务并且抛出RejectedExecutionException异常。 // DiscardPolicy():如果线程池队列满了,会直接丢掉这个任务并且不会有任何异常 // DiscardOldestPolicy():丢弃队列中最老的任务,队列满了,会将最早进入队列的任务删掉腾出空间,再尝试加入队列 executor.setRejectedExecutionHandler(new ThreadPoolExecutor.DiscardPolicy()); executor.initialize(); return executor; } } } //最大线程数 executor.setMaxPoolSize(maxPoolSize); //核心线程数 executor.setCorePoolSize(corePoolSize); //任务队列的大小 executor.setQueueCapacity(queueCapacity); //线程前缀名 executor.setThreadNamePrefix(namePrefix); //线程存活时间 executor.setKeepAliveSeconds(keepAliveSeconds);
//此处的名字要与线程池的名字一致,清晰的命名可以清晰的了解哪个线程报错 @Async("threadName") @GetMapping("/方法名字") public Result<String> test() { }
6、拒绝策略
rejectedExectutionHandler参数字段用于配置绝策略,常用拒绝策略如下
AbortPolicy
:用于被拒绝任务的处理程序,它将抛出RejectedExecutionExceptionCallerRunsPolicy
:用于被拒绝任务的处理程序,直接在execute方法的调用线程中运行被拒绝的任务。DiscardOldestPolicy
:用于被拒绝任务的处理程序,放弃最旧的未处理请求,然后重试execute。DiscardPolicy
:用于被拒绝任务的处理程序,默认情况下它将丢弃被拒绝的任务。
7、线程池处理流程
线程池的工作流程(必知必会)?
这个问题回答的时候,最好用讲故事的方式进行。 假如核心线程数是5,最大线程数是10,阻塞队列也是10
- 1)有新任务来的时候,将先使用核心线程执行;
- 2)当任务数达到5个的时候,第6个任务开始排队;
- 3)当任务数达到15个的时候,第16个任务将开启新的线程执行,也就是第6个线程
- 4)当任务数达到20个的时候,线程池满了,如果有第21个任务,将执行拒绝策略
8、 Hutool 的ThreadUtil.execute
execute
方法是通过 GlobalThreadPool
来执行任务的,而 GlobalThreadPool
在 Hutool 中是一个全局的线程池。
它会使用默认的配置来初始化线程池,这些默认配置在 Hutool 内部已经设定好了,因此在使用 execute
方法时不需要手动指定核心池大小和最大池大小。
这种方法适合简单的任务执行,如果需要更灵活的线程池配置(比如自定义线程数、队列类型等),可以考虑使用 newExecutor
等方法手动创建线程池,并进行更详细的配置。
spring的 ThreadPoolTaskExecutor
:
- 优点: Spring 的
ThreadPoolTaskExecutor
是 Spring 框架提供的一个线程池实现,它提供了很多配置选项,允许你更加灵活地定制线程池的行为,比如核心线程数、最大线程数、队列容量、线程存活时间等。 - 适用情况: 适合在 Spring 环境中使用,对于基于 Spring 的项目,使用这种方式可以充分利用 Spring 提供的特性和管理能力,例如可以方便地集成到 Spring 的任务调度中
Hutool ThreadUtil
:
- 优点: Hutool 提供的
ThreadUtil
类是一个简化了的工具类,提供了一些静态方法来方便地创建线程池和执行任务。它是一个轻量级的工具库,适合在一般的 Java 程序中使用,不依赖于 Spring 框架。 - 适用情况: 适合非 Spring 项目或者不需要集成到 Spring 容器管理的场景。如果你不需要太多的线程池配置选项,而只是简单地执行任务,使用
ThreadUtil
的execute
方法会更加便捷。
常见线程池
- 1)定长线程池(FixedThreadPool)
- 2)定时线程池(ScheduledThreadPool)
- 3)可缓存线程池(CachedThreadPool)
- 4)单线程化线程池(SingleThreadExecutor)
核心概念:这四个线程池的本质都是ThreadPoolExecutor对象(看源码)
不同点在于:
- 1)FixedThreadPool:只有核心线程,线程数量固定,执行完立即回收,任务队列为链表结构的有界队列。
- 2)ScheduledThreadPool:核心线程数量固定,非核心线程数量无限,执行完闲置 10ms 后回收,任务队列为延时阻塞队列。
- 3)CachedThreadPool:无核心线程,非核心线程数量无限,执行完闲置 60s 后回收,任务队列为不存储元素的阻塞队列。
- 4)SingleThreadExecutor:只有 1 个核心线程,无非核心线程,执行完立即回收,任务队列为链表结构的有界队列
线程池的主要参数有哪些(必知必会)?
- 1)corePoolSize(必需):核心线程数。默认情况下,核心线程会一直存活,但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
- 2)maximumPoolSize(必需):线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞。
- 3)keepAliveTime(必需):线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
- 4)unit(必需):指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
- 5)workQueue(必需):任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。
- 6)threadFactory(可选):线程工厂。用于指定为线程池创建新线程的方式。
- 7)handler(可选):拒绝策略。当达到最大线程数时需要执行的饱和策略。
线程池的工作流程(必知必会)?
这个问题回答的时候,最好用讲故事的方式进行。 假如核心线程数是5,最大线程数是10,阻塞队列也是10
- 1)有新任务来的时候,将先使用核心线程执行;
- 2)当任务数达到5个的时候,第6个任务开始排队;
- 3)当任务数达到15个的时候,第16个任务将开启新的线程执行,也就是第6个线程
- 4)当任务数达到20个的时候,线程池满了,如果有第21个任务,将执行拒绝策略
线程安全
函数、函数库在并发环境中被调用时,能够正确地处理多个线程之间的共享变量,使程序功能正确完成。
- 并发:(由cpu分配时间片,看似像同时干多个事情实际是由不同的cpu时间片干)任务执行时间比调度频率长,会导致任务堆积,任务完成后,才能开始下一个任务,可能会造成任务被阻塞,直到有空闲线程可用。当任务执行时间比调度频率长时,会出现并发问题,线程池中的线程会被任务堵塞,直到前一个任务执行完成。
- 并行:当系统有一个以上CPU时,当一个CPU执行一个进程时,另一个CPU可以执行另一个进程,两个进程互不抢占CPU资源,可以同时进行,这种方式我们称之为并行(Parallel)。
eg:并发是两个队伍交替使用一台咖啡机。并行是两个队伍同时使用两台咖啡机。
- 线程和进程:下载为进程,下载的多个视频为线程,共享线程资源
- 共享变量:指的是多个线程都可以操作的变量
保存在堆
和方法区
中的变量就是Java中的共享变量
堆中存放new出来的对象,(包括实例变量); 栈中存放正在调用的方法中的局部变量 (包括方法的参数); 方法区中存储.class 字节码 文件(包括 静态变量 、静态方法)
public class Variables { // 类变量 共享变量 private static int a; //成员变量 共享变量 private int b; //局部变量 c和d 非共享变量 public void test(int c){ int d; } } //多线程场景,对于变量a和b的操作是需要考虑线程安全的,而对于线程c和d的操作是不需要考虑线程安全的。
线程不安全
多线程并发执行某个代码时,产生了逻辑上的错误,结果和预期值不相同 线程安全是指多线程执行时没有产生逻辑错误,结果和预期值相同
//开启了两个线程,每个线程执行1000次循环,循环中对count进行加1操作。等待两个线程都执行完成后,打印count的值。 public class Test { private static int count; private static class Thread1 extends Thread { public void run() { for (int i = 0; i < 1000; i++) { count ++; try { Thread.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } } } public static void main(String[] args) throws InterruptedException { Thread1 t1 = new Thread1(); Thread1 t2 = new Thread1(); t1.start(); t2.start(); //main主线程内调用join()方法:休眠主线程,等待t1、t2线程执行完毕主线程再继续,即最后输出count值 t1.join(); t2.join(); System.out.println(count); } }
输出结果应该是2000但是不有时候1996
count++的指令在实际执行的过程中不是原子性的,而是要分为读、改、写三步来进行;即先从内存中读出count的值,然后执行+1操作,再将结果写回内存中,如下图所示。
上图中线程1执行了两次自加操作,而线程2执行了一次自加操作,但是count却从6变成了8,只加了2。
我们看一下为什么会出现这种情况。当线程1读取count的值为6完成后,此时切换到了线程2执行,线程2同样读取到了count的值为6,而后进行改和写操作,count的值变为了7;此时线程又切回了线程1,但是线程1中count的值依然是线程2修改前的6,这就是问题所在!即线程2修改了count的值,但是这种修改对线程1不可见,导致了程序出现了线程不安全的问题,没有符合我们预期的逻辑。
导致线程不安全的原因
主要有三点:
- 不满足原子性:一个或者多个操作在 CPU 执行的过程中被中断,当 cpu 执行一个线程过程时,调度器可能调走CPU,去执行另一个线程,此线程的操作可能还没有结束;(synchronized锁解决)
- 不满足可见性:一个线程对共享变量的修改,另外一个线程不能立刻看到
- 不满足有序性:程序执行的顺序没有按照代码的先后顺序执行三、怎样解决线程不安全
Java中的原子操作包括:
- 除long和double之外的基本类型的赋值操作
- 所有引用reference的赋值操作
- java.concurrent.Atomic.* 包中所有类的一切操作
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
volatile
关键字来保证可见性
。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。
volatile
关键字来保证一定的“有序性”
。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。另外,Java内存模型具备一些先天的“有序性”,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。
补充:happens-before原则(先行发生原则)
- 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
- 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作。
- volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作。
- 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C。
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作。
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
- 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行。
- 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始。
总结
以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。