为了做好运维面试路上的助攻手,特整理了上百道 【运维技术栈面试题集锦】 ,让你面试不慌心不跳,高薪offer怀里抱!
这次整理的面试题,小到shell、MySQL,大到K8s等云原生技术栈,不仅适合运维新人入行面试需要,还适用于想提升进阶跳槽加薪的运维朋友。
本份面试集锦涵盖了
- 174 道运维工程师面试题
- 128道k8s面试题
- 108道shell脚本面试题
- 200道Linux面试题
- 51道docker面试题
- 35道Jenkis面试题
- 78道MongoDB面试题
- 17道ansible面试题
- 60道dubbo面试题
- 53道kafka面试
- 18道mysql面试题
- 40道nginx面试题
- 77道redis面试题
- 28道zookeeper
总计 1000+ 道面试题, 内容 又全含金量又高
- 174道运维工程师面试题
1、什么是运维?
2、在工作中,运维人员经常需要跟运营人员打交道,请问运营人员是做什么工作的?
3、现在给你三百台服务器,你怎么对他们进行管理?
4、简述raid0 raid1raid5二种工作模式的工作原理及特点
5、LVS、Nginx、HAproxy有什么区别?工作中你怎么选择?
6、Squid、Varinsh和Nginx有什么区别,工作中你怎么选择?
7、Tomcat和Resin有什么区别,工作中你怎么选择?
8、什么是中间件?什么是jdk?
9、讲述一下Tomcat8005、8009、8080三个端口的含义?
10、什么叫CDN?
11、什么叫网站灰度发布?
12、简述DNS进行域名解析的过程?
13、RabbitMQ是什么东西?
14、讲一下Keepalived的工作原理?
15、讲述一下LVS三种模式的工作过程?
16、mysql的innodb如何定位锁问题,mysql如何减少主从复制延迟?
17、如何重置mysql root密码?
网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。
一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!
为什么引入线程池技术?
对于服务端的程序,经常面对的是执行时间较短、工作内容较为单一的任务,需要服务端快速处理并返回接口。假若服务端每次接收到一个任务,就创建一个线程,然后执行,这种方式在原型阶段是不错的选择,但是面对成千上万的任务提交进服务器时,这个时候将会创建数以万记的线程,这很明显不是一个好的选择。为什么呢?
- 第一,频繁的线程切换会使操作系统频繁的进行上下文切换,增加了系统的负载;
- 第二,线程的创建和销毁是需要耗费系统资源的,这样子很明显浪费了系统资源。
线程池技术很好的解决了这个问题,它预先创建一定数量的线程,用户不能直接控制线程的创建和销毁,重复使用固定或者较为固定数目的线程来完成任务的执行。这样做的好处:
- 消除了频繁创建和销毁线程的系统资源开销;
- 面对过量任务的提交能够平缓劣化。
ThreadPoolExcutor源码解析
在看具体的源码之前,先给一个线程池使用案例
- 创建线程池对象;
executor.submit(Runnable task)
提交10个任务;executor.submit(Callable<T> task)
提交5个任务;- 所有线程的管理都由线程池来原理,程序员不需要关注线程的创建销毁。
构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
RejectedExecutionHandler handler) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), handler);}
核心参数:
corePoolSize
:核心线程数,线程池里一直不会被销毁的线程数量;maximumPoolSize
:最大线程数量;keepAliveTime
:非核心线程空闲时的存活时间,该参数只有在线程数量 > corePoolSize情况下才有用;unit
:keepAlive时间单位;workQueue
:工作队列,JDK提供这几种工作队列:
-
ArrayBlockingQueue
:基于数组的有界阻塞队列,任务以FIFO顺序排序; -
LinkedBlockingQueue
:基于链表的阻塞队列,任务以FIFO顺序排列,吞吐量优于ArrayBlockingQueue,在使用时需要注意,此阻塞队列在不设置大小的时候,默认的长度是Integer.MAX_VALUE
; -
PriorityBlockingQueue
:类似于LinkedBlockQueue,但其所含任务的排序不是FIFO,而是依据任务的自然排序顺序或者是构造函数的Comparator决定的顺序; -
SynchronousQueue
:特殊的BlockingQueue,对其的操作必须是放和取交替完成的,典型的生产者-消费者模型,它不存储元素,每一次的插入必须要等另一个线程的移除操作完成。threadFactory
:创建线程工厂,可以自定义线程工厂给线程池里的线程设置一个自定义线程名。
handler
:饱和策略,假如线程池已满,并且没有空闲的线程,这个时候不再允许提交任务到线程池,线程池提供了4中策略,至于具体采用哪种策略还是自定义策略,具体情况具体分析。
-
AbortPolicy
:拒绝提交,直接抛出异常,也是默认的饱和策略; -
CallerRunsPolicy
:线程池还未关闭时,用调用者的线程执行任务; -
DiscardPolicy
:丢掉提交任务; -
DiscardOldestPolicy
:线程池还未关闭时,丢掉阻塞队列最久为处理的任务,并且执行当前任务。
线程池内部状态
线程池用ctl的低29位表示线程池中的线程数,高3位表示当前线程状态,后续假如想要增大这个值,可以将AtomicInteger改成AtomicLong。
- RUNNING:运行状态,高3位为111;
- SHUTDOWN:关闭状态,高3位为000,在此状态下,线程池不再接受新任务,但是仍然处理阻塞队列中的任务;
- STOP:停止状态,高3位为001,在此状态下,线程池不再接受新任务,也不会处理阻塞队列中的任务,正在运行的任务也会停止;
- TIDYING:高3位为010;
- TERMINATED:终止状态,高3位为011。
接下来就以submit
方法入手,分析一下相关源码。
submit任务提交
public Future<?> submit(Runnable task) {
//提交的task为null,抛出空指针异常
if (task == null)
throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);
//执行任务
execute(ftask);
return ftask;
}
整个任务的提交核心都在任务执行这部分,执行任务,拿到返回值。
任务执行execute
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
} else if (!addWorker(command, false))
reject(command);
}
具体的执行流程如下:
- 通过
workerCountOf
计算出当前线程池的线程数,如果线程数小于corePoolSize
,执行addWork
方法创建新的线程执行任务; - 如果当前线程池线程数大于
coreSize
,向队列里添加task,不继续增加线程; - 当
workQueue.offer
失败时,也就是说现在队列已满,不能再向队列里放,此时工作线程大于等于corePoolSize
,创建新的线程执行该task; - 执行
addWork
失败,执行reject
方法处理该任务。
总结一下,对于使用线程池的外部来说,线程池的机制是这样的:
- 如果正在运行的线程数 <
coreSize
,马上创建线程执行该task,不排队等待; - 如果正在运行的线程数 >=
coreSize
,把该task放入队列; - 如果队列已满 && 正在运行的线程数 <
maximumPoolSize
,创建新的线程执行该task; - 如果队列已满 && 正在运行的线程数 >=
maximumPoolSize
,线程池调用handler的reject方法拒绝本次提交。
addWorker实现
从全局来看,ThreadPoolExcutor一定维护一个池:
addWorker的实质是向该HashSet里add一个worker,worker有一个线程,这个线程执行完成时,会从该HashSet里remove掉。
看一下addWorker的具体代码实现:
这只是addWorker的前半部分代码,首先,判断当前线程的状态是否符合条件,不符合条件不做处理直接返回;通过参数core判断当前线程是否为核心线程,如果是核心线程,跳出循环创建新的线程来执行该task,在break retry的时候会执行compareAndIncrementWorkerCount(c)
,利用CAS原则,将线程数量+1。
看看创建线程部分(addWorker的后半部分)代码实现:
创建线程部分最核心的操作就是:new一个新的worker,add进HashSet,然后启动woker里的Thread。
从源码可以看到,在执行add之前先活取了mainLock锁,该锁是一个公用的可重入锁:
private final ReentrantLock mainLock = new ReentrantLock();
addWorker的4种调用方式
addWorker(command, true)
线程数 < coreSize时,将task放入workers,如果线程数 >= coreSize,返回false;addWorker(command, false)
当阻塞对列已满,尝试将新的task放入workers,如果线程数 >= maximumPoolSize,返回false;addWorker(null, false)
放入一个空的task到workers,此时线程数的限制是maximumPoolSize,相当于创建一个新的线程,没立马分配任务;addWorker(null, true)
放入一个空的task到workers,线程数 < coreSize。实际的使用是在prestartCoreThread()
等方法,有兴趣的读者可以自行阅读,在此不做详细赘述。
Worker具体实现
先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前在阿里
深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新Linux运维全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上运维知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
mg-gNzLFv3C-1715885070700)]
[外链图片转存中…(img-YVO2088V-1715885070701)]
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上运维知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新