2️⃣java基础进阶——多线程、并发与线程池的基本使用
一、概念介绍
什么是线程,什么是进程,两者有什么关系?
进程是操作系统资源分配的独立单位;而线程是操作系统能够进行调度和分派的最小单位;线程包含于进程之中,是进程中的实际运作单位。
例如:
正在运行的360安全卫士就属于一个进程
使用360卫士中的各个功能时,就属于开启一个线程
1.Thread类与Runnable接口
为什么有了Thread类还要有Runnable接口,两者各有什么用途?
他们两个最主要的区别:一个是接口一个是实现类,常用接口可以避免单继承的局限性外,具体区别是:继承Thread类的方式可能会导致类的局部变量不能正确的被共享。因为每个线程都是一个独立的对象,它们之间不能共享实例变量,如果需要共享变量,就必须使用静态变量或共享对象锁。而使用Runnable接口的方式,多个线程可以共享同一个Runnable实例,从而共享实例变量。
使用Runnable接口可以更好的体现面向对象编程的思想,把任务和线程分离开来。可以把任务当作一个对象,而线程可以看作是该对象(任务)的执行者。这样就可以更好的实现模块化设计,提高代码的可重用性和可维护性。
使用Runnable接口可以更好地处理多个线程之间的交互和协作。因为实现了Runnable接口的实例可以作为参数传递给Thread类的构造函数,因此线程(一个Thread实例)可以共享同一个Runnable实例,并且多个线程可以同时执行同一个Runnable实例的不同方法,从而实现多个线程之间的交互和协作。总的来说,使用Runnable接口的方式更加灵活和通用,可以实现更多的多线程编程场景,更好的体现面向对象编程的思想,提高代码的可重用性和可维护性。
但是,使用Thread类的方式也有其适用的场景,例如在一些简单的多线程编程场景中可以使用继承Thread类的方式来实现。
2.并发和并行的区别
(1).处理任务不同
并发(Concurrent)是一个CPU同时处理多个线程任务。(宏观上是同时处理多个任务,微观上其实是CPU在多个线程之间快速的交替执行。操作系统中有一个组件叫做任务调度器,它将CPU的时间片分配给各个线程使用,在一个时间段的线程运行时,其他线程处于挂起状态,这种就称之为并发。
并行(parallel)是多个CPU处理器同时处理多个线程任务。(当一个CPU执行一个线程时,另一个CPU可以执行另一个线程,两个线程互不抢占CPU资源,可以同时进行,这就被称之为并行。)
(2).CPU资源不同
并发过程中,线程之间会去抢占CPU资源,轮流便用。并行过程中,线程间不会抢占CPU资源。(因为是多个CPU处理器,各做各的。)
3.什么是线程池,使用线程池有哪些优势?
什么是线程池?
线程池,是一种线程使用模式,线程池维护着多个线程,等待着管理者为其分配可并发执行的任务,就是可管理、维护和分配线程的“池子”。
为什么使用线程池?
为了减少创建和销毁线程的次数,让每个线程郜可以多次的使用;
利用线程池可以根据系统情况调整线程的数量,防止消耗过多内存。在实际使用中,服务器在创建和销毁线程上花费的时间和系统资源都相当大,使用线程池就可以优化这些消耗。
通俗的说:使用线程池就是为了让线程对象可以反复的复用,不需要每次执行任务时构建一个新的线程,等到任务处理完后再销毁。
(1)使用线程池的优势:
(2)线程池的应用场景
二、Java内置线程池的使用方法
(一)线程池类:ThreadPoolExecutor的介绍
我们要想自定义线程池,必须先了解线程池的工作原理,才能自己定义线程池 这里我们通过观察java中ThreadPoolExecutor的源码来学习线程池的原理 (源码演示在idea中查看)
1.ThreadPoolExecutor的构造方法(了解这七个核心参数)
public ThreadPoolExecutor(int corePoolSize, //核心线程数量int maximumPoolSize, //最大线程数long keepAliveTime, //最大空闲时间TimeUnit unit, //时间单位BlockingQueue<Runnable> workQueue,//任务队列ThreadFactory threadFactory, //线程工厂RejectedExecutionHandler handler //饱和处理机制)
-
int corePoolSize:核心线程数量
-
int maximumPoolSize:最大线程数
-
long keepAliveTime:最大空闲时间,当线程空闲的时长达到该时间时,会被自动回收。
-
TimeUnit unit:设置最大空闲时间的时间单位
-
BlockingQueue<Runnable>workQueue:任务队列,当线程池中所有线程都不空闲时,线程池在收到任务时,会在该队列中等候;
-
ThreadFactory threadFactory:线程工厂
-
RejectedExecutionHandler handler:饱和处理机制,当线程池中的线程已达到最大线程数量,且都在工作;并且任务队列上也已经排满等候的任务时,证明当前线程池已经饱和,此时再来任务将会触发该机制。
2.线程池的工作流程
我们通过下面一个场景来理解ThreadPoolExecutor中的各个参数,以及线程池的工作流程:
a客户(任务)去银行(线程池)办理业务,但银行刚开始营业,窗口服务员还未就位(相当于线程池中初始线程数量为0),于是经理(线程池管理者)就安排1号工作人员(创建1号线程执行任务)接待a客户(创建线程);
在a客户业务还没办完时,b客户(任务)又来了,于是经理(线程池管理者)就安排2号工作人员(创建2号线程执行任务)接待b客户(又创建了一个新的线程) 假设该银行总共就2个窗口(核心线程数量是2);
紧接着在a,b客户都没有结束的情况下客户来了,于是经理(线程池管理者)就安排c客户先坐到银行大厅的座位上(空位相当于是任务队列)等候,并告知他: 如果1、2号工作人员空出,c就可以前去办理业务;
此时d客户又到了银行,(工作人员都在忙,大厅座位也满了)于是经理赶紧安排临时工(新创建一个线程)在大堂站着,手持pad设备给d客户办理业务;
假如前面的业务都没有结束的时候e客户又来了,此时正式工作人员都上了,临时工也上了,座位也满了(临时工加正式员工的总数量就是最大线程数),于是经理只能按《超出银行最大接待能力处理办法》(饱和处理机制)拒接接待e客户;
最后,进来办业务的人少了,大厅的临时工空闲时间也超过了1个小时(最大空闲时间),经理就会让这部分空闲的临时员工下班。(销毁线程)
但是为了保证银行正常工作(有一个alowCoreThreadTimeout变量控制是否允许销毁核心线程,默认false),即使正式员工闲着,也不得提前下班,所以1、2号工作人员继续待着(池内保持核心线程数量);
3. 线程池的4个参数的设计:
(1)核心线程数(corePoolSize)
核心线程数的设计需要依据任务的处理时间和每秒产生的任务数量来确定,例如:
执行一个任务需要0.1秒,系统百分之80的时间每秒都会产生100个任务,那么要想在1秒内处理完这100个任务,就需要10个线程,此时我们就可以设计核心线程数为10;当然实际情况不可能这么平均,所以我们一般按照8020原则设计即可,既按照百分之80的情况设计核心线程数,剩下的百分之20可以利用最大线程数处理。
(2)任务队列长度(workQueue)
任务队列长度一般设计为:核心线程数/单个任务执行时间*2即可;例如上面的场景中,核心线程数设计为10,单个任务执行时间为 0.1秒,则队列长度可以设计为200。
(3)最大线程数(maximumPoolSize)
最大线程数的设计除了需要参照核心线程数的条件外,还需要参照系统每秒产生的最大任务数决定:例如:上述环境中,如果系统每秒最大产生的任务是1000个。那么,最大线程数=(最大任务数-任务队列长度)单个任务执行时间;既:最大线程数=(1000-200)0.1=80个。
(4)最大空闲时间(keepAliveTime)
这个参数的设计完全参考系统运行环境和硬件压力设定,没有固定的参考值,用户可以根据经验和系统产生任务的时间间隔合理设置一个值即可。
注意:上面4个参数的设置只是一般的设计原则,并不是固定的,用户也可以根据实际情况灵活调整!
4.线程池类:ThreadPoolExecutor的实际使用
1.自定义线程池-实现步骤
-
(1)编写任务类(MyTask),实现Runnable接口;
-
(2)编写线程类(MyWorker),用于执行任务,需要持有所有任务:
-
(3)编写线程池类(MyThreadPool),包含提交任务,执行任务的能力;
-
(4)编写测试类(MyTest),创建线程池对象,提交多个任务测试; (具体代码参考idea)
小提示:关于线程池的功能比较繁多,这里仅仅模拟了核心功能,其他功能大家可以自行考补全;