当前位置: 首页 > 后端技术 > Java

吃透线程池原理——40行从头写线程池

时间:2023-04-01 22:37:16 Java

吃透线程池原理——40行从头写线程池前言在我们日常的编程中,并发永远是分开的不是一个开放的话题,在并发多线程中,线程池是一个无法回避的问题。多线程可以提高我们并发程序的效率,让我们不用频繁申请和释放线程,这是一个很大的开销,但是在线程池中,不需要频繁申请线程。他的主要原理是申请了线程之后,不中断,而是不断的去队列中接收任务,然后执行,重复这样的操作。这篇文章主要介绍线程池的原理,所以我们自己写一个非常非常简单的线程池,主要是帮助大家理解线程池的核心原理!!!线程池提供的功能先来看一个使用线程池的例子:){ExecutorServicepool=Executors.newFixedThreadPool(5);for(inti=0;i<100;i++){pool.execute(newRunnable(){@Overridepublicvoidrun(){for(inti=0;i<100;i++){System.out.println(Thread.currentThread().getName()+"print"+i);}}});在上面的例子中,我们使用Executors.newFixedThreadPool来生成一个线程数固定的线程池。上面的代码中,我们使用了5个线程,然后通过execute方法不断的向线程池提交任务。池中的任务队列添加任务,线程池中的线程会不断的从任务队列中取出任务,然后执行,然后继续取任务继续执行....线程执行过程为如下:while(true){Runnablerunnable=taskQueue.take();//从任务队列中取出任务runnable.run();//执行任务}根据上面提到的内容,现在我们的需求很明确了,首先我们要有一个队列用来存放我们需要的任务,然后需要开多个线程不断的从中取任务任务队列,然后执行它们,然后反复获取任务来执行操作工具。在我们前面提到的线程池实现原理中,有一个非常重要的数据结构,就是ArrayBlockingQueue阻塞队列。它是一种并发且安全的数据结构。我们先简单介绍一下如何使用这个数据结构。(如果想深入了解阻塞队列的实现原理,可以参考这篇文章JDK数组阻塞队列源码分析)我们主要使用ArrayBlockingQueue的以下两个方法:put函数,这个函数是往队列中添加数据线。我们需要了解的是,如果一个线程调用这个函数向队列中添加数据,如果此时队列已满,则需要挂起线程,如果未满,则需要向队列中添加数据队列,即数据存入数组。take函数从队列中取出数据,但是当队列为空时,需要阻塞调用该方法的线程。当队列中有数据时,可以从队列中取出数据。需要注意的是,如果一个线程被以上两个线程中的任何一个阻塞,可以通过调用对应线程的中断方法来终止该线程的执行,同时会抛出异常。下面是一段测试代码:<整数>(5);//队列容量为5Threadthread=newThread(()->{for(inti=0;i<10;i++){try{queue.put(i);System.out.println("Data"+i+"被添加到队列");}catch(InterruptedExceptione){System.out.println("发生中断异常");//如果发生中断异常,线程退出put方法将不会一直暂停return;}finally{}}});thread.start();TimeUnit.SECONDS.sleep(1);线程中断();}}以上代码输出结果:数据0加入队列数据1加入队列数据2加入队列数据3加入队列数据4加入队列发生中断异常的执行顺序上面的代码是:threadthread将0-4这5个数据加入到队列中,但是当第6个数据加入时,阻塞队列已满,所以thread线程在加入数据时会被阻塞,然后主线程在休息一秒后中断thread线程,然后thread线程有中断异常,然后捕获并进入catch生成代码阻塞,然后函数返回,thread线程不会一直阻塞,这在我们后面写线程池的时候很重要!!!WorkerDesign在上一篇文章中,我们提到了我们的线程需要不断的去任务队列中取出任务,然后执行。我们设计了一个Worker类来做到这一点!首先,类中必须有一个线程池任务队列,因为worker需要不断的从阻塞队列中取出任务执行。我们用一个isStopped变量来表示线程是否需要终止,即线程池是否需要关闭。如果线程池需要关闭,那么线程也应该停止。我们还需要一个变量来记录执行任务的线程,因为当我们需要关闭线程池时,我们需要等待任务队列中的所有任务完成,那么当所有任务都执行完后,队列必须是空的。而如果此时有线程去取任务,肯定会被阻塞。使用ArrayBlockingQueue的方法前面已经讲过了。我们可以使用这个线程的中断方法来中断这个线程的执行。这个线程会出现异常,然后这个线程捕获这个异常就可以退出,所以我们要知道在那个线程上执行interrupt方法!Worker实现的代码如下:importjava.util.concurrent.ArrayBlockingQueue;publicclassWorkerimplementsRunnable{//用来保存任务的队列privateArrayBlockingQueuetasks;//线程的状态是否终止privatevolatilebooleanisStopped;//保存执行run方法的线程privatevolatileThreadthisThread;publicWorker(ArrayBlockingQueuetasks){//这个参数是this.tasks=从线程池传入的tasks;}@Overridepublicvoidrun(){thisThread=Thread.currentThread();while(!isStopped){try{Runnabletask=tasks.take();任务.run();}catch(InterruptedExceptione){//donothing}}}//注意其他线程调用this同时需要注意的是thisThread执行完上面的run方法后会异常//其他线程调用interrupt方法这个线程往往这样就不会一直阻塞//会判断isStopped是否为true如果为true则可以退出while循环publicvoidstop(){isStopped=true;这个线程.中断();//中断线程thisThread}publicbooleanisStopped(){returnisStopped;}}线程池设计首先,线程池需要能够指定有多少个线程和阻塞队列的最大长度,所以我们需要有这两个参数。线程池必须有一个队列来存储和通过提交函数任务提交。需要有一个变量来存放所有的woker,因为当线程池关闭的时候,需要停止这些worker,也就是调用worker的stop方法。需要有一个shutDown函数来表示线程池关闭。需要有一个函数可以停止所有线程的执行,因为关闭线程池就意味着停止所有线程的工作。线程池实现代码:importjava.util.ArrayList;导入java.util.concurrent.ArrayBlockingQueue;publicclassMyFixedThreadPool{//用于存储任务的阻塞队列privateArrayBlockingQueuetaskQueue;//将所有线程保存在线程池中privateArrayListthreadLists;//线程池是否关闭privatebooleanisShutDown;//线程池中的线程数privateintnumThread;publicMyFixedThreadPool(inti){this(Runtime.getRuntime().availableProcessors()+1,1024);}publicMyFixedThreadPool(intnumThread,intmaxTaskNumber){this.numThread=numThread;taskQueue=newArrayBlockingQueue<>(maxTaskNumber);//创建一个阻塞队列threadLists=newArrayList<>();//把所有worker全部保存for(inti=0;i{System.out.println(Thread.currentThread().getName());//提交的任务是打印线程本身的名称});}pool.shutDown();}}上面的代码可以正常执行结束,但是输出太长这里只列出部分输出结果:ThreadPool-Thread-0ThreadPool-Thread-4ThreadPool-Thread-0ThreadPool-Thread-1ThreadPool-Thread-3ThreadPool-Thread-1ThreadPool-Thread-3ThreadPool-Thread-3ThreadPool-Thread-3ThreadPool-Thread-3ThreadPool-Thread-3ThreadPool-Thread-2ThreadPool-Thread-3ThreadPool-Thread-2ThreadPool-Thread-1ThreadPool-Thread-0ThreadPool-Thread-0ThreadPool-Thread-0ThreadPool-Thread-1ThreadPool-Thread-4从上面的输出我们可以看出线程池中只有5个线程,而这5个线程不断从队列中取出任务并执行,因为我们可以看到多次输出同一个线程的名字。小结本文主要介绍了线程池的原理,以及我们应该如何设计线程池。如何使用ArrayBlockingQueue,队列中一个非常重要的数据结构。线程池中有一个阻塞队列,用于存放所有提交给线程池的任务。所有Worker都会不断的从任务队列中取出任务并执行。线程池的shutdown方法其实更难去想怎么实现。首先,我们需要在真正关闭线程池之前完成任务队列中的所有任务,然后停止所有线程。所有任务都执行完后,有的线程可能会阻塞在take方法上(从队列中取数据的方法,如果队列为空,线程就会阻塞),幸运的是,ArrayBlockingQueue在实现的时候就考虑到了这个问题。只要其他线程调用阻塞线程的interrupt方法,该线程就可以通过捕获异常恢复执行,然后判断isStopped。如果需要停止,就会跳出while循环,这样我们就可以完成所有线程的停止操作。以上就是本文的全部内容,我是LeHung,我们下期再见!!!更多精彩内容合集可以访问项目:https://github.com/Chang-LeHu...关注公众号:一个没用的研究僧,学习更多计算机知识(Java,Python,计算机系统基础,算法和数据结构)知识。