java面试题-线程、线程池的了解及工作原理、拒绝策略

2023-12-13 07:23:09
远离八股文,面试大白话,通俗且易懂

看完后试着用自己的话复述出来。有问题请指出,有需要帮助理解的或者遇到的真实面试题不知道怎么总结的也请评论中写出来,大家一起解决。

java面试题汇总-目录-持续更新中

这篇还蛮好理解的,看一下,争取一篇搞懂

多线程的了解

?首先,先做一个前提就是:大家肯定听到过多线程。通俗来讲呢就是一个程序可以同时执行多个线程。就像是一个人它可以写字也可以画画,单线程就是先写字,完成后再画画。后来他觉得太慢了,就练就了本领,左手写字右手画画,这个就叫多线程。

拓展:为什么经常说多线程不安全,因为人只有一个脑子,有时候你左手写字的时候脑子在想着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(更新策略):

丢弃任务队列中最旧的任务,然后尝试重新将新任务加入队列。就是把队列里面排队最早的直接给丢掉,然后把这个给加入任务。

这几个策略有各自的场景:

默认策略用的比较多,比如我捕获到异常,然后记录在一张表里面,等高峰期过去了,通过定时任务再给拿出来重新执行。

另外第四个用的也比较多。比如说一个场景就是,一辆行驶在公里上的汽车,不停的上报经纬度。系统拿到经纬度然后做一些业务。这时候队列里已经排满了,但是最早的一条数据可能是几分钟之前的了,他肯定不如当前最新的数据有用,所以,就把最早的丢掉,处理最新的。

总结线程池的工作原理

首先用户提交任务,先判断核心线程数是否已满,如果未满则创建新线程来执行任务。如果核心线程数已满,则判断是否达到最大线程数,如果已达到最大线程数,则通过拒绝策略处理,如果未达到最大线程数,则将任务放入工作队列等待调用。空闲的线程如果没有新的任务可以执行,等待时间到达后就会被回收掉。

文章来源:https://blog.csdn.net/qq_36178165/article/details/134947701
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。