> 文章列表 > 线程池杂记

线程池杂记

线程池杂记

1.线程与进程

程序启动的是一个进程,一个进程里面包含一个及以上的线程,线程是CPU的最小单位,进程之间互不干涉,线程之间可以共享部分资源,线程包含程序计数器(包含线程下一步指令的代码段内存位置)、栈内存(记录存储着局部变量的位置),线程基本信息(线程id,线程名称、线程状态、线程优先级(优先级越高,获得CPU的概率越大))
下面总结一下进程与线程的区别,主要有以下几点:
(1)线程是“进程代码段”的一次顺序执行流程。一个进程由一个或多个线程组成,一个进程至少有一个线程。
(2)线程是CPU调度的最小单位,进程是操作系统分配资源的最小单位。线程的划分尺度小于进程,使得多线程程序的并发性高。
(3)线程是出于高并发的调度诉求从进程内部演进而来的。线程的出现既充分发挥了CPU的计算性能,又弥补了进程调度过于笨重的问题。
(4)进程之间是相互独立的,但进程内部的各个线程之间并不完全独立。各个线程之间共享进程的方法区内存、堆内存、系统资源(文件句柄、系统信号等)。
(5)切换速度不同:线程上下文切换比进程上下文切换要快得多。所以,有的时候,线程也称为轻量级进程。

2.线程的四种创建方式

2.1 基础thread 实现run()方法 没有返回值和异常
2.2 实现runable接口 实现run()方法 没有返回值和异常
2.3 使用callable和futureTask结合创建异步线程;callable是一个泛型的函数接口,执行其call,返回其泛型数据,并存在异常处理;callable 接口与thread线程的实例关联接口为runnableFuture接口,该接口继承runable,future接口,其中futrue接口提供三个注意的功能:1.取消异步执行中的任务,2.判断异步任务是否执行完成,3.获取异步任务完成后的结果,而主要的获取线程结果的实现类为futureTask,通过FutureTask类和Callable接口的联合使用可以创建能够获取异步执行结果的线程,具体步骤如下:
(1)创建一个Callable接口的实现类,并实现其call()方法,编写好异步执行的具体逻辑,可以有返回值。
(2)使用Callable实现类的实例构造一个FutureTask实例。
(3)使用FutureTask实例作为Thread构造器的target入参,构造新的Thread线程实例。
(4)调用Thread实例的start()方法启动新线程,启动新线程的run()方法并发执行。其内部的执行过程为:启动Thread实例的run()方法并发执行后,会执行FutureTask实例的run()方法,最终会并发执行Callable实现类的call()方法。
(5)调用FutureTask对象的get()方法阻塞性地获得并发线程的执行结果
2.4 通过线程池来创建线程 Executors工厂类来创建线程,ExecutorService线程池的execute(…)与submit(…)方法的区别如下。
(1)接收的参数不一样submit()可以接收两种入参:无返回值的Runnable类型的target执行目标实例和有返回值的Callable类型的target执行目标实例。而execute()仅仅接收无返回值的target执行目标实例,或者无返回值的Thread实例。
(2)submit()有返回值,而execute()没有submit()方法在提交异步target执行目标之后会返回Future异步任务实例,以便对target的异步执行过程进行控制,比如取消执行、获取结果等。execute()没有任何返回,target执行目标实例在执行之后没有办法对其异步执行过程进行控制,只能任其执行,直到其执行结束。

3.线程的基本状态

3.1线程的基本操作
1.setName 初始化线程的名称。线程池的名称尽可能不要重复,起一些便于回溯的名称,方便排查问题
2.sleep 让正在进行的线程进行休眠,让CPU去执行其他方法,线程的状态从执行状态变成限时阻塞状态
3.interrupt 线程有stop的方法,但java设置过时,其原因为,当调用stop的方法时,当前线程会直接关闭,此时,我们无法指定线程的状态,强行的终止,会导致一下不可预估的结果,例如当前线程在执行数据库的操作,突然间的stop的操作,数据库的操作并未全部执行完,将会导致数据库数据不一致。而interrupt的方法,只是给当前线程设置以中断状态,当线程是阻塞的时候,就会立马退出阻塞,抛出异常InterruptException,后续需要捕获异常,对其进行处理,如果现在在运行状态时,只会给其线程打上标签为终止线程,我们可以在其调用过程中,调用isInterrupt来查看线程是否处于中断状态,并执行中断操作。
4.join 线程合并,线程A需要将线程B的执行流程合并到自己的执行流程当中,其中线程A进入TIMED_WAITING等待状态中,等线程B运行结束后,线程A在编程RUNNABLE状态中,继续执行自身的过程,线程A可以进行限时等待,如果在限时的时间内,线程B没有执行完成,线程A也将开始执行自己的程序。
5.waiting 线程等待状态,表示线程需要等待唤醒,不会占用分配CPU时间。有两种方式可以是线程进入等待状态,①join 可以是线程A进入wait状态;②wait 方法也可以使当前线程进入wait状态,对应的唤醒方式为:Object.notify()/Object.notifyAll()
6.timed_waitingl 线程等待唤醒 线程处于等待唤醒状态,不分配CPU时间片,线程要等待唤醒,或者直到等待的时限到期
7.yield 线程让步,当前线程让出CPU的执行权限,使得CPU去执行其他的线程,但现在状态在JVM里面是RUNNABLE状态,但是在应用操作系统里面当前线程的状态就会从执行状态变成就绪状态,由于放弃和重占CPU时间不确定,可能会出现,刚让步就再次获得CPU的执行权限,然后重新开始。让步操作,只能让线程从执行状态变成就绪状态,而不是堵塞状态,也不能使当前运行线程立马变成就绪状态,及时在就绪状态,CPU在选择的时候,也可能在次挑选到本次线程,在调度的过程,受到线程的其他影响,如优先级。
3.2守护线程
java 线程分为,守护线程和用户线程,守护线程也称后台线程,专门指在程序进程运行过程中,在后台提供某种服务的线程,例如每一个JVM都存在一个GC线程一样。其中GC就称为守护线程。一个线程如果将setDaemon设置为true时,当前线程就为守护线程。并且在守护线程里面创建的线程都为守护线程。
其中守护线程和用户线程的关系
1.守护线程是为用户线程服务的后台系统;
2.用户线程与JVM进程是,用户线程终止,JVM进程也会随之终止;守护线程是JVM线程终止,从而会导致所有的守护线程而终止。
3.守护线程必须在线程start的时候回,将Deamon设置为true,在运行过程设置就会报异常。
4.守护线程是被JVM强制终止的,因此守护线程尽量不执行一些访问资源的文件,防止JVM强制终止,从而导致资源的不可逆的损失。
3.3线程的6中状态。
NEW状态(新建)---->RUNNABLE状态(就绪)---->BLOCKED状态(阻塞)---->WAITING状态(等待)---->TIMED_WAITING状态(等待唤醒)---->TERMINATED状态(终止)

4.线程池的原理

4.1线程池的作用
(1)提升性能:线程池能独立负责线程的创建、维护和分配。在执行大量异步任务时,可以不需要自己创建线程,而是将任务交给线程池去调度。线程池能尽可能使用空闲的线程去执行异步任务,最大限度地对已经创建的线程进行复用,使得性能提升明显。
(2)线程管理:每个Java线程池会保持一些基本的线程统计信息,例如完成的任务数量、空闲时间等,以便对线程进行有效管理,使得能对所接收到的异步任务进行高效调度。
4.2Executors创建线程池的方法
1.Executors.newSingleThreadExecutor()创建只有一个线程的线程池,线程池能够保证所有任务安装指定顺序执行,线程的执行任务按照任务提交的次序而执行,并且线程池的唯一线程的存活时间是无限存活,当线程池的唯一线程在繁忙中时,新提交的任务会进入一个无界的阻塞队列中,缺点:创建的工作队列为Integer.Max_VALUE,导致大量任务对压,会出现OOM(即耗尽内存资源)
2.Executors.newFixedThreadPool(int nThreads)创建一个固定大小的线程池,如果线程的数量没有达到固定的数量,那么每提交一个任务,线程池里面就会创建一个新的线程,直到线程达到线程池的固定数量,当到达固定数据的线程是,再次提交任务回将任务放到一个无界的阻塞队列中,并且,线程池里面的线程因为异常而导致线程异常而结束,线程池会重新创建一个新的线程来补齐线程固定数的,缺点:创建的工作队列为Integer.Max_VALUE,导致大量任务对压,会出现OOM(即耗尽内存资源)
3.Executors.newCachedThread()创建一个不限制线程数量的线程池,任何提交的任务都可以立刻执行,但是空闲线程会得到及时回收,以接收新的任务为实例,如果当前线程在繁忙中,线程池会添加新的任务线程来处理改任务,而且线程池不限制大小,完全有JVM能创建的最大线程数量来决定,存在部分空闲线程,即没有任务的线程,会在60s不执行任务,将会被回收,缺点:运行创建的线程数量为Integer.Max_VALUE,导致创建很多线程,出现OOM
4.Executors.newScheduledThreadPool()创建一个可定期或者执行延时的线程池,缺点:运行创建的线程数量为Integer.Max_VALUE,导致创建很多线程,出现OOM
4.3标准的线程池创建方式
1.采用ThreadPoolExecutor构造器来创建线程池,其中需要主要的7个参数,分别是:
1.1核心和最大线程数 corePoolSize 用于设置核心线程池的数量,maxmumPoolSize设置最大线程数量,线程池中的线程数量,小于核心线程数时,新的任务提交时,线程池会自己创建线程来处理其请求,即时有线程空闲,当线程池的线程数量大于核心线程数小于最大线程数时,新的任务请求进入时,会放入到任务队列中,仅当任务队列满时,才创建新的线程来执行器任务,超过最大线程数时,会抛出异常
1.2 BolckingQueue 阻塞队列,用于暂时接收新的异步任务,阻塞队列目前主要于一下几种:1. ArrayBlockingQueue 数组有界阻塞队列,元素按照FOFO排序,创建是必须指定大小;2.LinkedBlockingQueue 链表阻塞队列,可以设置为有界和无界(Integer.Max_VALUE)两种,吞吐量高于ArrayBlockingQueue;3.PriorityBlockingQueue 优先级的无界队列;4.DelayQueue 无界延迟队列,每个元素都有过期时间,当从队列获取元素时,只是以及过去的元素任务才会出队列执行;5.SynchronousQueue 同步队列是一个不存储元素的阻塞队列,每插入一个元素必须等另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量高于LinkedBlockingQueue
1.3 keepAliveTime 空闲线程存活时间, 超过其时间值,非核心的线程将会被回收
4.4向线程池提交的方式目前有两种,①调用execute();②调用submit();两种的区别主要有:1.execu只能接收Runnable类型的参数,不允许有返回值,不会抛出异常;submit可以接收Callable,Runnable两种类型的参数,有返回值,并且允许抛出异常,它是通过返回Future对象来回去返回值,或者捕获异常
4.5 线程池的任务调度流程
(1)如果当前工作线程数量小于核心线程数量,执行器总是优先创建一个任务线程,而不是从线程队列中获取一个空闲线程。
(2)如果线程池中总的任务数量大于核心线程池数量,新接收的任务将被加入阻塞队列中,一直到阻塞队列已满。在核心线程池数量已经用完、阻塞队列没有满的场景下,线程池不会为新任务创建一个新线
程。
(3)当完成一个任务的执行时,执行器总是优先从阻塞队列中获取下一个任务,并开始执行,一直到阻塞队列为空,其中所有的缓存任务被取光。
(4)在核心线程池数量已经用完、阻塞队列也已经满了的场景下,如果线程池接收到新的任务,将会为新任务创建一个线程(非核心线程),并且立即开始执行新任务。
(5)在核心线程都用完、阻塞队列已满的情况下,一直会创建新线程去执行新任务,直到池内的线程总数超出maximumPoolSize。如果线程池的线程总数超过maximumPoolSize,线程池就会拒绝接收任务,当新任务过来时,会为新任务执行拒绝策略。
4.6线程池的拒绝策略,当线程池出现一下两种情况就会执行RejectedExecutionHandler中的rejectedExecution方法;① 线程池以及关闭②工作队列已满且maximumPoolSize已满,目前存在一下几种实现1.AbortPolicy拒绝策略,线程池队列满了,新任务就会拒绝,线程池默认的,抛出异常;2.DiscardPolicy抛弃策略,线程池队列满了,新任务就会拒绝,不抛出异常;3.DiscardOldPolicy抛弃最老任务测试,队列满了的话,就会将最早进入的任务抛弃;4.CallerRunsPolicy调用这执行策略,新任务添加到线程池时,如果失败,那么调教任务的线程就会自己执行该任务,不会使用线程池里面的线程去执行;5.自定义策略,实现RejectedExecutionHandler中的rejectedExecution方法即可
4.6线程池优雅关闭
可以结合shutdown()、shutdownNow()、awaitTermination()三个方法优雅地关闭一个线程池,大致分为以下几步:
(1)执行shutdown()方法,拒绝新任务的提交,并等待所有任务有序地执行完毕。
(2)执行awaitTermination(long timeout,TimeUnit unit)方法,指定超时时间,判断是否已经关闭所有任务,线程池关闭完成。
(3)如果awaitTermination()方法返回false,或者被中断,就调用shutDownNow()方法立即关闭线程池所有任务。
(4)补充执行awaitTermination(long timeout,TimeUnit unit)方法,判断线程池是否关闭完成。如果超时,就可以进入循环关闭,循环一定的次数(如1000次),不断关闭线程池,直到其关闭或者循环结束。
使用钩子关闭线程池
static
{
//注册JVM关闭时的钩子函数
Runtime.getRuntime().addShutdownHook(
new ShutdownHookThread(“定时和顺序任务线程池”,
new Callable()
{
@Override
public Void call() throws Exception
{
//优雅地关闭线程池
shutdownThreadPoolGracefully(EXECUTOR);
return null;
}
}));
}

5.线程池里面线程数的确认

目前线程的任务可以大致的分为三类
5.1 IO密集型任务 执行IO操作的任务,IO操作任务的时间较长,导致CPU利用率不高;因此通常需开CPU核心数的两倍,当IO线程空闲时,可以启用其他线程继续执行使用CPU,以提高CPU的使用率。
5.2 CPU密集型任务 执行计算任务,由于响应时间较快,CPU一致处于运行状态,利用率很高,这个时候,线程数等于CPU数就行。
5.3 混合型任务 及执行逻辑计算又要进行IO操作,业界有一个比较成熟估算公式,最佳线程数=((线程等待时间+线程CPU时间)/线程CPU时间)*CPU核数

6.ThreadLocal 线程局部变量

如果程序创建了一个ThreadLocal实例,那么在访问这个变量时,每个线程都会拥有一个独立的、自己的本地值,ThreadLocal可以看成专属线程的变量,不受其他线程干扰,保存着线程的专属数据,当线程结束后,每个线程所拥有的那个本地值就会被释放,在多线程并发操作ThreadLocal的时候,线程各自操作的是自己本地的值,从而规避了线程安全的问题,ThreadLocal实例可以理解一个Map集合,其中key值为Thread实例,value为保存的值。
6.1使用场景
1.线程隔离
ThreadLocal主要的价值在与线程隔离,它只属于当前线程,其本地值对别的线程是不可见,多并发的情况下,可以防止自己的变量被其他线程篡改,由于各线程之间的数据是相互隔离的,同时也避免了加锁带来的性能损失,大大提高了并发的性能。
2.跨函数传递数据
通常用于同一个线程内,跨类、跨方法传递数据时,如果不用ThreadLocal,那么相互之间的数据传递势必要靠返回值和参数,这样无形之中增加了这些类或者方法之间的耦合度。由于ThreadLocal的特性,同一线程在某些地方进行设置,在随后的任意地方都可以获取到。线程执行过程中所执行到的函数都能读写ThreadLocal变量的线程本地值,从而可以方便地实现跨函数的数据传递。使用ThreadLocal保存函数之间需要传递的数据,在需要的地方直接获取,也能避免通过参数传递数据带来的高耦合。