关于java:一个线程的打工故事

52次阅读

共计 4445 个字符,预计需要花费 12 分钟才能阅读完成。

前言

前几天小强去阿里巴巴面试 Java 岗,止步于二面。

他和我诉苦本人被虐的多惨多惨,特地是深挖线程和线程池的时候,竟然被问到不晓得如何作答。

对于他的遭逢,联合他过了一面的那个嘚瑟样,我深表同情(加大力度)!

好了,不开玩笑了,在和小强的面试题中,我选取了几个比拟典型的线程和线程池的问题。

Java 中的线程和操作系统的线程有什么关系?

调用 start 办法是如何执行 run 办法的?

线程池提交工作有哪几种形式?别离有什么区别?

谈谈你对阻塞队列的了解。

常见的线程池有哪些?为什么阿里不容许应用 Executors 去创立线程池?

线程池任务调度的流程大抵讲一下。

线程池外面的线程执行异样了会怎么样?

外围线程和非核心线程是如何辨别的?

想要答对这些问题,并不是很难,然而想要答好,我感觉是十分考验集体功底的。

为了弄清这些问题,我连夜加急,采访了“线程”,上面是线程的自述。

我是谁

我是一个线程,一个底层的打工人。

总有人把我和过程搞混,但其实我和过程的区别很大。

过程是程序的一次执行,CPU 的资源都是分发给过程而不是分发给咱们线程,过程是资源分配的最小单位,一个过程能够蕴含很多向我这样的线程。

咱们线程是 CPU 调度执行的最小单位,真正的打工人。

Java 中的线程

在 Java 外面,我的名字叫做 java.lang.Thread。

须要留神的是,调用 run 办法和执行一个一般办法没有区别。想要真正的创立一个线程并启动,须要调用我的 start 办法。

有一点我必须通知你,就是我也是有小弟的。

在 JVM 外面,我有一个 JavaThread 的小弟,他帮我分割操作系统的 osthread 线程。

调用我的 start 办法之后,具体的执行流程是这样的:

当然了,这个过程省略了很多细节,不过很明确的是,我和内核线程是一一对应的。

调度我就相当于调度内核线程,而调度内核线程须要在用户态和内核态之间切换,这个过程开销是十分大的。

所以,创立我老本是很高的,肯定要谨慎。

线程池

和你们人类一样,我也有着精彩的毕生,也会经验出世(创立)、奋斗(Running)、死亡(销毁)等过程,明天我次要和你讲述的是我打工奋斗的生存。

原来我是打零工的,有人须要我的时候就创立一个我,等我实现工作就把我销毁。

下面也提到过,我和内核线程是一对一的,创立和销毁的过程是十分耗费资源的,所以这样的老本十分高。

于是,有人就想了一个方法,开了一个公司,也就是你们说的线程池。

线程池公司对立治理调度咱们线程。咱们在线程池外面反复着 期待工作——实现工作 的步骤。

这样我就能够日复一日年复一年的反复打工了,这种提供了缩小对象数量从而改善利用所需的对象构造的形式的模式,被你们人类叫做“享元模式”。

线程池公司有很多种,但都离不开这几个次要指标:

  • corePoolSize:公司正式员工人数。
  • maximumPoolSize:正式工 + 临时工最大数量。
  • keepAliveTime:临时工多久没做事件会被开革。
  • unit:临时工没做事件会被开革的工夫单位。
  • workQueue:公司业务接管部门。
  • threadFactory:行政部,负责招聘培训员工的。
  • handler:业务部接管业务达到下限了的解决形式。

阻塞队列

线程池中的 workQueue 是一个阻塞队列,用于寄存线程池未能及时处理执行的工作。

它的存在既解耦了工作的提交与执行,又能起到一个缓冲的作用。

阻塞队列有很多,上面我带你理解一下常见的阻塞队列。

ArrayBlockingQueue

基于数组实现的有界阻塞队列,创立的时候须要指定容量。此类型的队列依照 FIFO(先进先出)的规定对元素进行排序。

LinkedBlockingQueue

基于链表实现阻塞队列,默认大小为 Integer.MAX_VALUE。依照 FIFO(先进先出)的规定对元素进行排序

SynchronousQueue

一个不存储元素的阻塞队列。每一个 put 操作必须阻塞期待其余线程的 take 操作,take 操作也必须期待其余线程的 put 操作。

PriorityBlockingQueue

一个基于数组利用堆构造实现优先级成果的无界队列,默认天然序排序,也能够本人实现 compareTo 办法自定义排序规定。

DelayedWorkQueue

一个实现了优先级队列性能且实现了提早获取的无界队列,在创立元素时,能够指定多久多久能力在队列中获取以后元素。只有延时期满了后能力从队列中获取元素。

回绝策略

当工作队列满了之后,如果还有工作提交过去,会触发回绝策略,常见的回绝策略有:

  • AbortPolicy:抛弃工作并抛出异样,默认该形式。
  • CallerRunsPolicy:由调用线程本人解决该工作。谁调用,谁解决。

  • DiscardPolicy:抛弃工作,然而不抛出异样。
  • DiscardOldestPolicy:摈弃工作队列中最旧的工作也就是最先退出队列的,再把这个新工作增加进去。先从工作队列中弹出最先退出的工作,空出一个地位,而后再次执行 execute 办法把工作退出队列。

当然,除了以上这几种回绝策略,你也能够依据理论的业务场景和业务需要去自定义回绝策略,只须要实现 RejectedExecutionHander 接口,自定义外面的 rejectedExecution 办法。

运行流程

咱们每个线程会被包装成 Worker,线程池外面有一个 HashSet 寄存 Worker。

当有工作提交过去之后:

  1. 首先检测线程池运行状态,如果不是 RUNNING,则间接回绝,线程池要保障在 RUNNING 的状态下执行工作。
  2. 如果线程池中 Worker 的数量小于外围线程数,就会去创立一个新的线程,也就是招聘一个正式工让他执行工作。
  3. 如果 Worker 的数量大于或者等于外围线程数,就会把工作放到阻塞工作队列外面。
  4. 如果工作队列满了还有工作过去,如果临时工名额没有满(workerCount < maximumPoolSize),就去招聘临时工让临时工执行工作。如果临时工名额都满了,触发工作回绝策略。

总结而言,就是外围线程无能的事件尽量不去创立非核心线程,这是线程池很要害的一点。

有哪些线程池

我有过四段工作经验,每段经验都有着精彩的故事。

SingleThreadExecutor

SingleThreadExecutor 是我退出的第一家线程池,这是一家守业公司,整个线程池就只有我一个线程。

所有的工作都由我干,而且工作队列是一个无界队列。就是说,打工的线程只有我一个,然而需要工作能够是有限多。

在需要工作很多的时候,经常出现工作解决不过去的状况,导致工作沉积,呈现 OOM。

但因为所有的活都是我干,没有繁琐的沟通老本,不须要解决线程同步的问题,这算是这种线程池的一个长处吧。

这种线程池实用于并发量不大且须要工作程序执行的场景。

FixedThreadPool

起初公司开张了,我又退出了一个叫 FixedThreadPool 的线程池。

FixedThreadPool 和 SingleThreadExecutor 惟一不同的中央就是外围线程的数量,FixedThreadPool 能够招收很多的打工线程。

在这里,我不再是孤军奋斗了,我有了一群独特打拼的小伙伴,大家一起实现工作,一起承当压力。

可这种线程池还是存在一个问题——工作队列是无界的,需要工作过多的话,还是会造成 OOM。

这种线程池线程数固定,且不被回收,线程与线程池的生命周期同步的线程池,实用于任务量比拟固定但耗时长的工作。

CachedThreadPool

起初,为了离家更近,我到职了。退出了一家叫 CachedThreadPool 的线程池,进去之后,却发现这是一家外包公司。

这种线程池外面没有一个外围线程(正式工),一有需要就去招聘一个非核心线程(临时工)。

如果一个线程工作干完了之后,60 秒之后没有新的工作就会被解雇。

这种线程池的工作队列采纳的是 SynchronousQueue,这个队列是无奈插入工作的,一有工作就创立一个线程执行,如果并发高且工作耗时长,创立太多线程也是可能导致 OOM 的。所以 CachedThreadPool 比拟适宜任务量大但耗时少的工作。

ScheduleThreadPool

经验了里面的风风雨雨,我感觉还是找份固定的工作比拟牢靠,于是我退出了一家叫做 ScheduleThreadPool 的国企。

在这里,工作比拟的轻松,少数状况下,我只须要在固定的工夫干固定的活。

工作忙不过来的时候,公司也会招聘一些临时工帮忙解决,临时工干完活就会被解雇。

综合来说,这类线程池实用于执行定时工作和具体固定周期的反复工作。因为采纳的工作队列是 DelayedWorkQueue 无界队列,所以也是有 OOM 的危险的。

总结

好了,对于线程的故事就告一段落了。对于线程池的利用实际,咱们下次再聊。

文章结尾的面试题在大部分在文中都能找到答案,对于没有提到的,这里做一个补充:

1. 线程池提交工作有哪几种形式?别离有什么区别?

有 execute 和 submit 两种形式

  • execute 只能提交 Runnable 类型的工作,无返回值。submit 既能够提交 Runnable 类型的工作,也能够提交 Callable 类型的工作,会有一个类型为 Future 的返回值,但当工作类型为 Runnable 时,返回值为 null。
  • execute 在执行工作时,如果遇到异样会间接抛出,而 submit 不会间接抛出,只有在应用 Future 的 get 办法获取返回值时,才会抛出异样。

2. 线程池外面的线程执行异样了会怎么样?

如果一个线程执行工作的过程中出现异常,那么这个线程对应的 Worker 会被移出线程池,该线程也会被销毁回收。

同时会通过指定的线程工厂创立一个线程,并封装成 Worker 放入线程池代替移除的 Worker。

3. 外围线程能被回收吗?

外围线程默认不会被回收。然而能够调用 allowCoreThreadTimeOut 让外围线程能够被回收。

须要留神的是,调用这个办法的线程池必须将 keepAliveTime 设置为大于 0,否则会抛出异样。

4. 外围线程和非核心线程是如何辨别的?

外围线程和非核心线程是一个抽象概念,只是用于更好的表述线程池的运行逻辑,实际上都对应操作系统的 osThread,都是重量级线程。

在新增 Worker 的时候,通过一个 boolean 表白是外围线程还是非核心线程,实质上两者没有什么不同。

5. 为什么阿里不容许应用 Executors 去创立线程池?

FixedThreadPool 和 SingleThreadPool:容许的申请队列长度为 Integer.MAX_VALUE,可能会沉积大量的申请,从而导致 OOM。

CachedThreadPool:容许的创立线程数量为 Integer.MAX_VALUE,可能会创立大量的线程,从而导致 OOM。

总结来说就是,应用 Executors 创立线程池会容易漠视线程池的一些属性,使用不当容易引起资源耗尽。

参考:《2020 最新 Java 根底精讲视频教程和学习路线!》

链接:https://juejin.cn/post/692433…

正文完
 0