一个线程的打工故事

简介: 一个线程的打工故事

我是谁

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

网络异常,图片无法展示
|

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

进程是程序的一次执行,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创建线程池会容易忽视线程池的一些属性,使用不当容易引起资源耗尽。

写在最后

这个世界上或许没有线程,又或许人人都是线程。

好了,今天的文章就到这里了。

最后,感谢你的阅读!

我是CoderW,一个普通的程序员。

点个关注,我们下期再见!

参考文章

目录
相关文章
|
缓存 Java 数据处理
科大爱情故事之线程池工厂打工记
科大爱情故事之线程池工厂打工记
83 0
|
5天前
|
存储 Linux C语言
c++进阶篇——初窥多线程(二) 基于C语言实现的多线程编写
本文介绍了C++中使用C语言的pthread库实现多线程编程。`pthread_create`用于创建新线程,`pthread_self`返回当前线程ID。示例展示了如何创建线程并打印线程ID,强调了线程同步的重要性,如使用`sleep`防止主线程提前结束导致子线程未执行完。`pthread_exit`用于线程退出,`pthread_join`用来等待并回收子线程,`pthread_detach`则分离线程。文中还提到了线程取消功能,通过`pthread_cancel`实现。这些基本操作是理解和使用C/C++多线程的关键。
|
7天前
|
安全 Java
【极客档案】Java 线程:解锁生命周期的秘密,成为多线程世界的主宰者!
【6月更文挑战第19天】Java多线程编程中,掌握线程生命周期是关键。创建线程可通过继承`Thread`或实现`Runnable`,调用`start()`使线程进入就绪状态。利用`synchronized`保证线程安全,处理阻塞状态,注意资源管理,如使用线程池优化。通过实践与总结,成为多线程编程的专家。
|
7天前
|
Java 开发者
告别单线程时代!Java 多线程入门:选继承 Thread 还是 Runnable?
【6月更文挑战第19天】在Java中,面对多任务需求时,开发者可以选择继承`Thread`或实现`Runnable`接口来创建线程。`Thread`继承直接但限制了单继承,而`Runnable`接口提供多实现的灵活性和资源共享。多线程能提升CPU利用率,适用于并发处理和提高响应速度,如在网络服务器中并发处理请求,增强程序性能。不论是选择哪种方式,都是迈向高效编程的重要一步。
|
7天前
|
Java 开发者
震惊!Java多线程的惊天秘密:你真的会创建线程吗?
【6月更文挑战第19天】Java多线程创建有两种主要方式:继承Thread类和实现Runnable接口。继承Thread限制了多重继承,适合简单场景;实现Runnable接口更灵活,可与其它继承结合,是更常见选择。了解其差异对于高效、健壮的多线程编程至关重要。
|
8天前
|
Java 程序员
Java多线程编程是指在一个进程中创建并运行多个线程,每个线程执行不同的任务,并行地工作,以达到提高效率的目的
【6月更文挑战第18天】Java多线程提升效率,通过synchronized关键字、Lock接口和原子变量实现同步互斥。synchronized控制共享资源访问,基于对象内置锁。Lock接口提供更灵活的锁管理,需手动解锁。原子变量类(如AtomicInteger)支持无锁的原子操作,减少性能影响。
20 3
|
7天前
|
Java
JAVA多线程深度解析:线程的创建之路,你准备好了吗?
【6月更文挑战第19天】Java多线程编程提升效率,通过继承Thread或实现Runnable接口创建线程。Thread类直接继承启动简单,但限制多继承;Runnable接口实现更灵活,允许类继承其他类。示例代码展示了两种创建线程的方法。面对挑战,掌握多线程,让程序高效运行。
|
7天前
|
Java 调度
【实战指南】Java多线程高手秘籍:线程生命周期管理,掌控程序命运的钥匙!
【6月更文挑战第19天】Java多线程涉及线程生命周期的五个阶段:新建、就绪、运行、阻塞和死亡。理解这些状态转换对性能优化至关重要。线程从新建到调用`start()`变为就绪,等待CPU执行。获得执行权后进入运行状态,执行`run()`。遇到阻塞如等待锁时,进入阻塞状态。完成后或被中断则死亡。管理线程包括合理使用锁、利用线程池、处理异常和优雅关闭线程。通过控制这些,能编写更高效稳定的多线程程序。
|
13天前
|
Java API
详细探究Java多线程的线程状态变化
Java多线程的线程状态主要有六种:新建(NEW)、可运行(RUNNABLE)、阻塞(BLOCKED)、等待(WAITING)、超时等待(TIMED_WAITING)和终止(TERMINATED)。线程创建后处于NEW状态,调用start()后进入RUNNABLE状态,表示准备好运行。当线程获得CPU资源,开始执行run()方法时,它处于运行状态。线程可以因等待锁或调用sleep()等方法进入BLOCKED或等待状态。线程完成任务或发生异常后,会进入TERMINATED状态。
|
13天前
|
存储 安全 Java
Java多线程中线程安全问题
Java多线程中的线程安全问题主要涉及多线程环境下对共享资源的访问可能导致的数据损坏或不一致。线程安全的核心在于确保在多线程调度顺序不确定的情况下,代码的执行结果始终正确。常见原因包括线程调度随机性、共享数据修改以及原子性问题。解决线程安全问题通常需要采用同步机制,如使用synchronized关键字或Lock接口,以确保同一时间只有一个线程能够访问特定资源,从而保持数据的一致性和正确性。