远离八股文,面试大白话,通俗且易懂
这篇还蛮好理解的,看一下,争取一篇搞懂
多线程的了解
首先,先做一个前提就是:大家肯定听到过多线程。通俗来讲呢就是一个程序可以同时执行多个线程。就像是一个人它可以写字也可以画画,单线程就是先写字,完成后再画画。后来他觉得太慢了,就练就了本领,左手写字右手画画,这个就叫多线程。
拓展:为什么经常说多线程不安全,因为人只有一个脑子,有时候你左手写字的时候脑子在想着123右手画画的时候不自觉地画出来了123就会导致数据混乱。计算机肯定不会这么糊涂,但是共享资源的情况下,可能一个线程还没结束另一个线程就把这个数据改掉了,就造成不安全。
线程和线程池的关系
还是通过举例先有个大概的了解:
比如你开了一家餐馆,线程就是厨师,而线程池就是你的餐馆后厨。中午突然来了很多客人点菜,这时候客人点一个菜你就去新雇佣一个厨师(就像程序中,来一个任务你就新创建一个线程)。这样不仅效率低下,而且成本很高(如果高峰期,一下进来一万个任务,你就要创建一万个线程,可能直接把服务器给干掉了)。
相反,你开了一家餐馆,你提前组织了一个厨师团队。来一个单子就交给一个厨师,再来一个单子就交给另外一个空闲的厨师。如果都忙着,那就进入等待,等其中一个厨师结束后,再把等待的单子启动。如果真的来的太多了,那就拒绝接单。这样效率高而且成本低。这就是线程和线程池的类比。
线程的创建
重要!!!
在 Java 中,有两种主要的方法来创建线程:继承 Thread
类和实现 Runnable
接口。
更推荐使用实现Runnable
接口的方式,因为java语言是单继承,所以Runnable
更加灵活。
1. 继承 Thread
类
//通过创建一个类,继承自 Thread 类,并重写 run() 方法来定义线程的执行逻辑。
//可以看到main方法中,new一个MyThread()就是创建一个线程。
class MyThread extends Thread {
public void run() {
// 线程执行的逻辑
System.out.println("Thread is running");
}
}
public class Main {
public static void main(String[] args) {
MyThread myThread = new MyThread();
myThread.start(); // 启动线程
}
}
2. 实现 Runnable
接口
class MyRunnable implements Runnable {
public void run() {
// 线程执行的逻辑
System.out.println("Runnable is running");
}
}
public class Main {
public static void main(String[] args) {
MyRunnable myRunnable = new MyRunnable();
Thread myThread = new Thread(myRunnable);
myThread.start(); // 启动线程
}
}
线程的状态
1.新建:通过new Thread()对象被创建,但还没启动的时候
2.就绪:通过start()方法调用后处于就绪状态,但不一定立即执行,因为要判断是否有足够资源去执行。
3.运行:run()方法被执行时的状态是运行。就是CPU有资源给到该线程,开始处理该线程任务。
4.阻塞:比如线程在等待某个锁(比如厨房只有一把菜刀,需要等另一个厨师用完你才能用)。或者是调用了sleep()方法的时候就进入阻塞状态。
5.等待:场景还是抢夺资源(厨房只有一把菜刀,你在用我就没办法用,这时候我就睡觉也就是Object.wait(),等你用完了,你来把我叫醒-通过notify()方法)然后我再继续执行。
4.超时等待:跟等待不同的是Object.wait(timeout),也就是我现在睡觉等菜刀,但是我定了个闹钟,比如我就等5分钟,五分钟一到不用你通知,我自己就醒了,不管有没有菜刀我就开始继续执行(有菜刀就继续没菜刀就报错嘛~~难道还用手切菜~)
5.线程结束:一个是线程顺利执行完了,另外一个就是线程报错了(比如第四步没有抢夺到资源)
线程池关键参数
1.核心线程数(Core Pool Size)
就是线程池的基本大小(厨房有几个厨师,核心线程数就是几,即使现在没有点菜的,几个厨师可能都闲着呢,也保持不变)
2.最大线程数(Maximum Pool Size)
就是线程池最大能承受多少线程,包括正在执行的线程还有等待的(厨房现在正在做着饭呢,还陆续有人来,比如厨房现在有三个厨师(核心数)同时做饭,但是又来了十来个人,那就排队等着。但是这家店太火了,突然来了一百个人,老板估算着说不行了,再来厨师快要累垮了,就说我们今天只接待20桌。那么20就是最大线程数,其他的就拒绝掉(拒绝策略))
3.线程存活时间(Keep Alive Time)
就是指线程池中,非核心线程能存活的多长时间。记得是非核心,因为核心线程是不会被回收的(比如说厨房有三个厨师,突然来了三个单子,那么这三个单子都是核心线程数。但是这时候又来了几个单子,厨师忙完手头的活一抬头看到黑板上还有三个单子,那么他们开始处理,等处理完了一抬头看到还是有三个,但是拿过来发现是之前已经做完的单子就又放回去,然后一会又抬头看到还有三个就拿过来一看还是刚才三个...这样循环下来不得气死?这时候老板就定了个时间,比如说五分钟一次,我看一下这几个单子是不是完成了,如果完成了我就给他划掉,这样厨师看的时候就看不到了。)节省资源
4.工作队列(Work Queue)
就是第三步说的单子,也就是如果线程数量达到了核心线程数,那么新来的就会被放入队列中进行等待。
5.拒绝策略(Rejected Execution Handler)
当无法接受新任务时(线程数已经达到最大线程数),定义了线程池的行为。例如,可以抛出异常、直接丢弃任务等。
线程池的拒绝策略
1.Abort Policy(默认策略)
当线程池无法接受新任务时,会抛出 RejectedExecutionException
异常。只需要捕获异常,你是重新提交还是丢弃还是记录等等随便。
2.Caller Runs Policy(回退策略)
就是哪个线程提交的任务我返回给谁。我现在已经超负荷了,你给我新的任务,我处理不了,我就返回给你,你自己执行吧。
3.Discard Policy(丢弃策略)
丢弃掉,不抛异常也不知道,直接pass掉。
4.Discard Oldest Policy(更新策略):
丢弃任务队列中最旧的任务,然后尝试重新将新任务加入队列。就是把队列里面排队最早的直接给丢掉,然后把这个给加入任务。
这几个策略有各自的场景:
默认策略用的比较多,比如我捕获到异常,然后记录在一张表里面,等高峰期过去了,通过定时任务再给拿出来重新执行。
另外第四个用的也比较多。比如说一个场景就是,一辆行驶在公里上的汽车,不停的上报经纬度。系统拿到经纬度然后做一些业务。这时候队列里已经排满了,但是最早的一条数据可能是几分钟之前的了,他肯定不如当前最新的数据有用,所以,就把最早的丢掉,处理最新的。
总结线程池的工作原理
首先用户提交任务,先判断核心线程数是否已满,如果未满则创建新线程来执行任务。如果核心线程数已满,则判断是否达到最大线程数,如果已达到最大线程数,则通过拒绝策略处理,如果未达到最大线程数,则将任务放入工作队列等待调用。空闲的线程如果没有新的任务可以执行,等待时间到达后就会被回收掉。