java多线程编程-02

294 阅读13分钟

1、线程池几种方式实现

多线程的实现方式

1.1:继承Thread类的方式

创建一个继承于Thread类的子类

重写Thread类中的run():将此线程要执行的操作声明在run()

创建Thread的子类的对象

调用此对象的start():①启动线程 ②调用当前线程的run()方法

1.2:实现Runnable接口的方式

创建一个实现Runnable接口的类

实现Runnable接口中的抽象方法:run():将创建的线程要执行的操作声明在此方法中

创建Runnable接口实现类的对象

将此对象作为参数传递到Thread类的构造器中,创建Thread类的对象

调用Thread类中的start():① 启动线程 ② 调用线程的run() --->调用Runnable接口实现类的run()

以下两种方式是jdk1.5新增的!

1.3:实现Callable接口

说明:

与使用Runnable相比, Callable功能更强大些

实现的call()方法相比run()方法,可以返回值

方法可以抛出异常

支持泛型的返回值

需要借助FutureTask类,比如获取返回结果

Future接口可以对具体Runnable、Callable任务的执行结果进行取消、查询是否完成、获取结果等。

FutureTask是Futrue接口的唯一的实现类

FutureTask 同时实现了Runnable, Future接口。它既可以作为Runnable被线程执行,又可以作为Future得到Callable的返回值

1.4:使用线程池

1.4.1、线程池的概念

线程池就是首先创建一些线程,它们的集合称为线程池。使用线程池可以很好地提高性能,线程池在系统启动时即创建大量空闲的线程,程序将一个任务传给线程池,线程池就会启动一条线程来执行这个任务,执行结束以后,该线程并不会死亡,而是再次返回线程池中成为空闲状态,等待执行下一个任务。

1.4.2、ThreadPoolExecutor

public ThreadPoolExecutor(int corePoolSize, //核心线程数量
int maximumPoolSize,// 最大线程数
long keepAliveTime, // 最大空闲时间
TimeUnit unit, // 时间单位
BlockingQueue workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 饱和处理机制
) \

20210308145342394.png

原文链接:blog.csdn.net/qq_43061290…
这里是7个参数(我们在开发中用的更多的是5个参数的构造方法),OK,那我们来看看这里七个参数的含义: corePoolSize  线程池中核心线程的数量 maximumPoolSize  线程池中最大线程数量 keepAliveTime 非核心线程的超时时长,当系统中非核心线程闲置时间超过keepAliveTime之后,则会被回收。如果ThreadPoolExecutor的allowCoreThreadTimeOut属性设置为true,则该参数也表示核心线程的超时时长 unit 第三个参数的单位,有纳秒、微秒、毫秒、秒、分、时、天等 workQueue 线程池中的任务队列,该队列主要用来存储已经被提交但是尚未执行的任务。存储在这里的任务是由ThreadPoolExecutor的execute方法提交来的。 threadFactory  为线程池提供创建新线程的功能,这个我们一般使用默认即可 handler 拒绝策略,当线程无法执行新任务时(一般是由于线程池中的线程数量已经达到最大数或者线程池关闭导致的),默认情况下,当线程池无法处理新线程时,会抛出一个RejectedExecutionException。 这7个参数中,平常最多用到的是corePoolSize、maximumPoolSize、keepAliveTime、unit、workQueue.在这里我主抽出corePoolSize、maximumPoolSize和 workQueue 三个参数进行详解。
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)
这个参数的设计完全参考系统运行环境和硬件压力设定,没有固定的参考值,用户可以根据经验和系统产生任务的时间间隔合理设置一个值即可; maximumPoolSize(最大线程数) = corePoolSize(核心线程数) + noCorePoolSize(非核心线程数);\

(1)当currentSize<corePoolSize时,没什么好说的,直接启动一个核心线程并执行任务。

(2)当currentSize>=corePoolSize 、并且workQueue未满时,添加进来的任务会被安排到workQueue中等待执行。

(3)当workQueue已满,但是currentSize<maximumPoolSize 时,会立即开

启一个非核心线程来执行任务。

(4)当currentSize>=corePoolSize 、workQueue已满、并且currentSize>maximumPoolSize时 ,调用handler默认抛出RejectExecutionExpection异常。

什么currentSize,corePoolSize,maximumPoolSize,workQueue比来比去的都比迷糊了,哈哈,那我举个烧烤店的例子来想必大家理解起来更快。

夏天了,很热,所以很多烧烤店都会在外面也布置座位,分为室内、室外两个地方可以吃烧烤。(室内有空调电视,而且室内比室外烧烤更加优惠,而且外面下着瓢泼大雨所以顾客会首先选择室内)

corePoolSize(烧烤店室内座位),cuurentPoolSize(目前到烧烤店的顾客数量),maximumPoolSize (烧烤店室内+室外+侯厅室所有座位),workQueue(烧烤店为顾客专门设置的侯厅室)

第(1)种,烧烤店人数不多的时候,室内位置很多,大家都其乐融融,开心的坐在室内吃着烧烤,看着世界杯。

第(2)种,生意不错,室内烧烤店坐无空席,大家都不愿意去外面吃,于是在侯厅室里呆着,侯厅室位置没坐满。

第(3)种,生意兴隆,室内、侯厅室都坐无空席,但是顾客太饿了,剩下的人没办法只好淋着雨吃烧烤,哈哈,好可怜。

第(4)种,生意爆棚,室内、室外、侯厅室都坐无空席,在有顾客过来直接赶走。

哈哈是不是很形象,对于workQueue还是有点陌生的小伙伴。

1.4.3、四种主要的线程池大概思路嫌上面使用线程池的方法太麻烦?

其实Executors已经为我们封装好了 4 种常见的功能线程池,如下: 用法在我推荐的博客里都有详细解说,在这里我就不一一道来了,在这里主要是跟大家分享一种特别容易记住这四种线程池的方法,在大家写代码,面试时可以即使想到这四种线程池。

  • 定长线程池(FixedThreadPool)
  • 定时线程池(ScheduledThreadPool )
  • 可缓存线程池(CachedThreadPool)
  • 单线程化线程池(SingleThreadExecutor) (1)FixedThreadPool:

Fixed中文解释为固定。结合在一起解释固定的线程池,说的更全面点就是,有固定数量线程的线程池。其corePoolSize=maximumPoolSize,且keepAliveTime为0,适合线程稳定的场所。

(2)SingleThreadPool:

Single中文解释为单一。结合在一起解释单一的线程池,说的更全面点就是,有固定数量线程的线程池,且数量为一,从数学的角度来看SingleThreadPool应该属于FixedThreadPool的子集。其corePoolSize=maximumPoolSize=1,且keepAliveTime为0,适合线程同步操作的场所。

(3)CachedThreadPool:

Cached中文解释为储存。结合在一起解释储存的线程池,说的更通俗易懂,既然要储存,其容量肯定是很大,所以他的corePoolSize=0,maximumPoolSize=Integer.MAX_VALUE(2^32-1一个很大的数字)

(4)ScheduledThreadPool:

Scheduled中文解释为计划。结合在一起解释计划的线程池,顾名思义既然涉及到计划,必然会涉及到时间。所以ScheduledThreadPool是一个具有定时定期执行任务功能的线程池。

2、多线程的应用场景

应用场景介绍
1:网购商品秒杀
2:云盘文件上传和下载
3:12306网上购票系统等
总之
只要有并发的地方、任务数量大或小、每个任务执行时间长或短的都可以使用线程池; blog.csdn.net/xiamiflying…

3、线程池的原理(以及线程池的大小评估)

4、Java线程池实现原理详解

5、线程池的使用和场景

线程池-四种拒绝策略总结

拒绝策略提供顶级接口 RejectedExecutionHandler ,其中方法 rejectedExecution 即定制具体的拒绝策略的执行逻辑。 jdk默认提供了四种拒绝策略:
CallerRunsPolicy - 当触发拒绝策略,只要线程池没有关闭的话,则使用调用线程直接运行任务。一般并发比较小,性能要求不高,不允许失败。但是,由于调用者自己运行任务,如果任务提交速度过快,可能导致程序阻塞,性能效率上必然的损失较大
AbortPolicy - 丢弃任务,并抛出拒绝执行 RejectedExecutionException 异常信息。线程池默认的拒绝策略。`**必须处理好抛出的异常,否则会打断当前的执行流程,影响后续的任务执行。
DiscardPolicy - 直接丢弃,其他啥都没有
DiscardOldestPolicy - 当触发拒绝策略,只要线程池没有关闭的话,丢弃阻塞队列 workQueue 中最老的一个任务,并将新任务加入

6、为什么会出现死锁

6.1死锁的概念

由于两个或者多个线程互相持有对方所需要的资源,导致线程处于等待状态,造成死锁。

6.2死锁出现的原因

  • 因为系统资源不足。
  • 进程运行推进的顺序不合适。
  • 资源分配不当等。

6.3死锁如何解决

  1. 破坏”互斥”条件:系统里取消互斥、若资源一般不被一个进程独占使用,那么死锁是肯定不会发生的,但一般“互斥”条件是无法破坏的,因此,在死锁预防里主要是破坏其他三个必要条件,而不去涉及破坏“互斥”条件。\
  2. 破坏“请求和保持”条件: 方法1:所有的进程在开始运行之前,必须一次性的申请其在整个运行过程各种所需要的全部资源。 优点:简单易实施且安全。 缺点:因为某项资源不满足,进程无法启动,而其他已经满足了的资源也不会得到利用,严重降低了资源的利用率,造成资源浪费。 方法2:该方法是对第一种方法的改进,允许进程只获得运行初期需要的资源,便开始运行,在运行过程中逐步释放掉分配到,已经使用完毕的资源,然后再去请求新的资源。这样的话资源的利用率会得到提高,也会减少进程的饥饿问题。\
  3. 破坏“不剥夺”条件:当一个已经持有了一些资源的进程在提出新的资源请求没有得到满足时,它必须释放已经保持的所有资源,待以后需要使用的时候再重新申请。这就意味着进程已占有的资源会被短暂的释放或者说被抢占了。\
  4. 破坏“循环等待”条件:可以通过定义资源类型的线性顺序来预防,可以将每个资源编号,当一个进程占有编号为i 的资源时,那么它下一次申请资源只能申请编号大于i 的资源。

7、线程的生命周期

线程的生命周期一共分为五个部分分别是:新建,就绪,运行,阻塞以及死亡。由于cpu需要在多条线程中切换因此线程状态也会在多次运行和阻塞之间切换

5ca3282b6c02e745.jpg

7、方法的定义是线程安全的?

8、如何保证100个线程完成的时候,在进行完成主线程?

9、如何判断一个线程已经结束了?

状态名称

说明

new

初始线程被构建,还未调用start()方法

runnable

运行,Java将就绪和运行笼统称为runnable

blocked

阻塞,阻塞于锁

waiting

等待,当前线程需要等待其他线程唤醒

timed_waiting

超时等待,超过一定时间可以自行苏醒

terminated

终止,线程执行完

方法1:通过Thread类中的isAlive()方法判断线程是否处于活动状态。

线程启动后,只要没有运行完毕,都会返回true。

【注】如果只是要等其他线程运行结束之后再继续操作,可以执行t.join(),即:在t执行完毕前挂起。

 

方法2:通过Thread.activeCount()方法判断当前线程的线程组中活动线程的数目,为1时其他线程运行完毕。

 

方法3:通过java.util.concurrent.Executors中的方法创建一个线程池,用这个线程池来启动线程。启动所有要启动的线程后,执行线程池的shutdown()方法,即在所有线程执行完毕后关闭线程池。然后通过线程池的isTerminated()方法,判断线程池是否已经关闭。线程池成功关闭,就意味着所有线程已经运行完毕了。

示例代码:

 1 import java.util.concurrent.ExecutorService;  
 2 import java.util.concurrent.Executors;  
 3 
 4 public class Test {  
 5   
 6     public static void main(String args[]) throws InterruptedException {  
 7         ExecutorService exe = Executors.newFixedThreadPool(50);  
 8         for (int i = 1; i <= 5; i++) {  
 9             exe.execute(new SubThread(i));  
10         }  
11    *   **  exe.shutdown();  
12         while (true) {  
13             if (exe.isTerminated()) {  
14                 System.out.println("结束了!");  
15                 break;  
16             }  ***
17             Thread.sleep(200);  
18         }  
19     }  
20 }

10、线程池的复用原理

线程池将线程和任务进行解耦,线程是线程,任务是任务,摆脱了之前通过 Thread 创建线程时的一个线程必须对应一个任务的限制。 在线程池中,同一个线程可以从阻塞队列中不断获取新任务来执行,其核心原理在于线程池对 Thread 进行了封装,并不是每次执行任务都会调用 Thread.start() 来创建新线程,而是让每个线程去执行一个“循环任务”,在这个“循环任务”中不停的检查是否有任务需要被执行,如果有则直接执行,也就是调用任务中的 run 方法,将 run 方法当成一个普通的方法执行,通过这种方式将只使用固定的线程就将所有任务的 run 方法串联起来。