当前位置: 首页 > 科技观察

深入源码分析Java线程池的实现原理

时间:2023-03-17 17:26:30 科技观察

程序的运行本质上就是对系统资源(CPU、内存、磁盘、网络等)的使用。如何高效地使用这些资源是我们编程优化进化的一个方向。今天说的线程池就是一种优化CPU利用率的手段。网上有很多文章介绍如何使用线程池,那么我想说什么呢?希望通过学习线程池的原理,了解所有池化技术的基本设计思想。其他类似问题可以解决。池化技术前面提到了一个名词——池化技术,那么池化技术到底是什么?简单来说,池化技术就是提前节省大量资源以备不时之需。在机器资源有限的情况下,使用池化技术可以大大提高资源利用率和性能。在编程领域,典型的池化技术有:线程池、连接池、内存池、对象池等,本文主要介绍比较简单的线程池的实现原理。希望读者能够举一反三,通过对线程池的理解,学习和掌握所有编程中池化技术的底层原理。创建线程在Java并发编程中非常重要。在Java中创建线程比较简单:publicclassApp{publicstaticvoidmain(String[]args)throwsException{newThread(newRunnable(){@Overridepublicvoidrun(){System.out.println("Threadisrunning");}}).start();}}我们可以通过创建线程对象并实现Runnable接口来实现一个简单的线程。可以利用多核CPU。当任务结束时,当前线程接收它。但很多时候,我们会执行不止一项任务。如果每次创建线程->执行任务->销毁线程都是这样,会造成很大的性能开销。是否可以创建一个线程,执行一个任务,执行另一个任务而不是销毁它。这就是线程池。这就是池化技术的思想。通过提前创建多个线程并放入池中,在需要线程的时候可以直接获取,避免了重复创建和销毁带来的开销。线程池中简单使用如下代码就是在Java中创建一个线程池:importjava.util.concurrent.*;publicclassApp{publicstaticvoidmain(String[]args)throwsException{ExecutorServiceexecutorService=newThreadPoolExecutor(1,1,60L,TimeUnit.SECONDS,newArrayBlockingQueue<>(10));executorService.execute(newRunnable(){@Overridepublicvoidrun(){System.out.println("abcdefg");}});executorService.shutdown();}}Jdk也提供了外部接口非常简单。直接调用ThreadPoolExecutor构造一个即可,也可以通过Executors静态工厂构造,但一般不推荐。可以看出,得益于Java为我们封装的一系列API,开发者在代码中使用线程池还是比较简单的。然而,这些API的背后到底是什么,让我们拨开这层迷雾,看清线程池的本质。线程池构造器通常,通用构造器会反映这个工具或这个对象的数据存储结构。构造函数把线程池比作一家公司。公司会有固定员工处理正常业务,如果工作量大,会聘请外包人员工作。外包人员可以从容释放,减少公司管理开销。由于成本的原因,公司总是雇用最多的人。如果此时还有任务处理不了,就去需求池安排任务。acc:获取调用上下文corePoolSize:核心线程数,可以类比为正式员工数和常驻线程数。maximumPoolSize:最大线程数,公司雇佣的最大员工数。驻留+临时线程数。workQueue:多余的任务在队列中等待,不管有多少人可以处理,都需要在这里等待等待。keepAliveTime:非核心线程的空闲时间,也就是外包商等了多久。如果没有工作可做,他们将被解雇。threadFactory:创建线程的工厂,在这里可以统一处理创建线程的属性。每个公司对员工的要求都不一样,好吧,在这里设置员工的属性。handler:线程池拒绝策略,什么意思?就是当任务多了,人不够了,需求池满了,还有任务怎么办?默认不处理,抛出异常告诉任务提交者,我太忙了。添加任务接下来我们看一下线程池中比较重要的execute方法,它是用来向线程池中添加任务的。源码核心模块用红框标注。第一个红框:workerCountOf方法根据ctl的低29位获取线程池当前线程数。如果线程数小于corePoolSize,则执行addWorker方法创建一个新线程执行任务;第二个红框:判断线程池是否在运行,如果是,任务队列是否允许插入,如果插入成功,再次验证线程池是否在运行。如果没有运行,则移除插入的任务,然后抛出拒绝策略。如果它正在运行并且没有剩余线程,则启用一个线程。第三个红框:如果添加非核心线程失败,直接拒绝。这里逻辑有点复杂,画个流程图参考一下。接下来我们看看如何添加工作线程?添加工作线程从方法execute的实现可以看出:addWorker主要负责创建新线程和执行任务。代码如下(这里的代码有点长,没关系,也是分块的,一共有5个关键代码块):***红框:是否可以添加workerthreads条件过滤器:判断线程池的状态,如果线程池的状态值大于等于SHUTDOWN,提交的任务将不会被处理,直接返回;第二个红框:做自旋,更新创建线程数:通过参数core判断当前要创建的线程是否是核心线程,如果为真,且当前线程数小于corePoolSize,则跳转退出循环并开始创建新线程。可能有人会疑惑什么是重试?这是java中的goto语法。它只能在break和continue之后使用。再看下面代码:第一个红框:获取线程池主锁。线程池的工作线程是通过Woker类实现的,通过ReentrantLock锁保证线程安全。第二个红框:给worker添加线程(在线程池中)。第三个红框:开始新线程。接下来,让我们看看工人是什么。一个哈希集。因此,线程池的底层存储结构实际上是一个HashSet。工作线程处理队列任务。第一个红框:是否是第一次执行任务,或者可以从队列中获取任务。第二个红框:获取任务后,执行任务开始前的operationhook。第三个红框:执行任务。第四个红框:执行任务后的钩子。这两个钩子(beforeExecute,afterExecute)让我们可以自己继承线程池,做任务执行前后的处理。到这里,源码分析到此结束。接下来做一个简单的总结。总结一下,所谓线程池的本质就是一个hashSet。多余的任务放在阻塞队列中。只有阻塞队列满了才会触发非核心线程的创建。所以非核心线程只是暂时在这里做些杂事。直到它空闲,然后自行关闭。线程池为我们提供了两个钩子(beforeExecute,afterExecute)。我们继承了线程池,在执行任务前后做了一些事情。线程池原理关键技术:锁(lock,cas)、阻塞队列、hashSet(资源池)***希望对大家理解线程池有所帮助。***,留下一个思考问题,为什么线程池的底层数据接口是用HashSet实现的?本文由作者投稿,原作者“临湾村龙猫”,霍利斯做了一些补充、调整和修改。【本文为专栏作家霍利斯原创文章,作者微信公众号Hollis(ID:hollishuang)】点此阅读更多本作者好文