之前,总觉得买东西,做东西,或者从某个时间点开始,人生就会出现一个转折点,一切都会很顺利。一下子变得强大起来。然而,这种情况并非如此。我不可能一下子变强,也不可能一口咬成胖子。看一篇文章,不能让你从此走上人生巅峰。我越来越相信,这是一个长期的过程,只有量变导致质变,即使缓慢,也绝不会停止。[TOC]如何设计线程池?三个步骤这是一个普遍的问题。如果对线程池的工作原理比较熟悉,这道题就不难了。要设计和实现一个事物,需要三个步骤:它是什么?为什么?怎么做?什么是线程池?线程池使用池化技术将线程存储在一个“池”(容器)中。当任务到来时,可以使用现有的空闲线程来处理它们。处理完成后,将它们放回容器中,可以重复使用。如果线程不够,可以按规则动态增加。当线程过多时,也可以杀死多余的线程。为什么要使用线程池?实现线程池有什么好处?减少资源消耗:池化技术可以复用创建的线程,减少线程创建和销毁的损失。提高响应速度:使用已有的线程进行处理,减少创建线程的时间可控线程:线程是稀缺资源,不能无限创建,线程池可以统一分配和监控扩展其他功能:比如定时线程池,要点定时执行任务需要考虑线程池设计需要考虑的点:线程池状态:有哪些状态?如何保持状态?线程是如何封装的?线程放在哪个池中?线程如何获取任务?线程有哪些状态?如何限制线程数?动态变化?自动缩放?线程是怎么死的?如何复用?可以直接处理的任务很少。当任务很多时,应该放在哪里?任务队列满了怎么办?使用什么队列?从任务阶段来看,分为以下几个阶段:如何保存任务?如何获得任务?如何执行任务?如何拒绝任务?线程池状态有哪些?如何保持状态?状态可以设置如下:RUNNING:运行状态,可以接受任务,也可以处理任务SHUTDOWN:不能接受任务,但是可以处理任务STOP:不能接受任务,也不能处理任务,中断当前任务TIDYING:AllthreadsstopTERMINATED:线程池的最终状态在各种状态之间是不同的。它们的状态之间的变化如下:维护状态可以用一个变量单独存储,需要保证修改的原子性。在底层操作系统中,对int的修改是原子的,但是在32位操作系统中,对double、long等64位值的操作不是原子的。另外,其实JDK中实现的状态和线程池中的线程数是同一个变量。高3位表示线程池的状态,低29位表示线程数。这样设计的好处是节省空间,同时更新的时候也有优势。如何封装线程相关的线程?线程放在哪个池中?线程实现Runnable接口。执行的时候会调用start()方法,但是start()方法内部会编译调用run()方法。该方法只能调用一次,调用多次会报错。所以,线程池中的线程运行后,是不可能终止和重启的,只能一直运行下去。既然不能停止,任务执行完之后,就没有任务过来了,只有轮询获取任务的进程线程才能运行任务,所以在封装线程的时候,假设封装成Worker,Worker必须包含一个Thread,表示当前Thread,除了当前线程,封装的线程类还应该持有任务,初始化可以直接给任务,只有当当前任务为null时才需要获取任务。可以考虑使用HashSet来存储线程,即充当线程池。当然HashSet会有线程安全问题需要考虑,那么我们可以考虑使用ReentrantLock等可重入锁。线程池中任何线程的添加或删除都需要加锁。privatefinalReentrantLockmainLock=newReentrantLock();线程如何获得任务?(1)在初始化线程时,可以直接指定任务,比如RunnablefirstTask,将任务封装到worker中,然后在worker中获取线程。thread.run()时,其实是运行worker本身的run()方法,因为worker本身实现了Runnable接口,里面的线程其实就是它自己。因此,也可以自定义ThreadFactory线程工厂。privatefinalclassWorkerextendsAbstractQueuedSynchronizerimplementsRunnable{finalThread线程;可运行的第一个任务;...Worker(RunnablefirstTask){setState(-1);//禁止中断直到runWorkerthis.firstTask=firstTask;//从线程池创建线程,传入的是自己的this.thread=getThreadFactory().newThread(this);}}(2)运行完任务的线程要继续取任务,取任务必须从任务队列中取。如果任务队列中的线程没有任务,因为是阻塞队列,可以等待。如果等待一定时间后仍然没有任务,如果线程池中的线程数已经超过核心线程数,任由线程死亡,则应将该线程从线程池中移出。并结束线程。获取和执行任务是线程池中线程的一个工作循环,除非它会死掉。线程有哪些状态?现在我们说的是Java中的线程Thread。线程在给定的时间点只能处于一种状态。这些状态是虚拟机的状态,不能反映任何操作系统的线程状态。有六种类型/七种状态:NEW:线程对象已经创建,但是还没有调用Start()方法,还没有启动的线程处于这种状态。Running:运行状态其实包括两种状态,但是Java线程将ready和running称为runnable线程池其中,等待调度,获得CPU使用权才有资格执行,执行完start()、endsleep()或join()后不一定进入就绪状态,线程会进入获得对象锁后的这个状态。当CPU时间片结束或者主动调用yield()方法时,也会进入这个状态Running:获得CPU的使用权(获得CPU时间片),变为运行状态BLOCKED:阻塞,线程被阻塞被锁,等待监视器锁,一般是通过Synchronize关键字修饰的方法或代码块WAITING:进入该状态,需要等待其他线程通知(notify)或中断,一个线程等待无限期地为另一个线程。TIMED_WAITING:等待超时,指定时间后自动唤醒,返回,不会永远等待TERMINATED:线程执行完毕,已经退出。如果在终止后调用start(),将抛出java.lang.IllegalThreadStateException。如何限制线程数?动态变化?自动缩放?线程池本身就是为了限制和充分利用线程资源,所以有两个概念:核心线程数和最大线程数。要让线程数根据任务数动态变化,那么我们可以考虑如下设计(假设有连续任务):为一个任务创建一个线程,直到线程数达到核心线程数。核心线程数达到后,没有空闲线程,进来的任务直接放入任务队列。如果任务队列是无界的,它就会爆炸。如果任务队列是有界的,任务队列满后,如果有任务过来,会继续创建线程进行处理。此时线程数大于核心线程数,直到线程数等于最大线程数。达到最大线程数后,还有任务过来,会触发拒绝策略,根据不同的策略进行处理。如果任务不断处理,任务队列为空,线程空闲,没有任务,则在一定时间内销毁,使线程数保持在核心线程数。从上面可以看出,控制缩放的主要参数是核心线程数、最大线程数、任务队列和拒绝策略。线程是怎么死的?如何复用?线程不能多次重新调用start(),所以只能调用一次,即线程不可能停止再启动。那么说明线程复用只是一个连续的循环而已。Extinction只是结束了它的run()方法。当需要自动减少线程池数量时,会结束一些空闲线程。而复用,其实就是任务执行完后,去任务队列中取任务。如果未获取任务,它将等待。任务队列是一个阻塞队列,是一个不断循环的过程。很少有任务相关的任务可以直接处理。当任务很多时,应该放在哪里?任务少的时候,来了就直接创建,给线程初始化任务,然后就可以开始执行了。当任务较多时,可以放入队列,先进先出。任务队列满了怎么办?当任务队列满时,会继续添加线程,直到达到最大线程数。使用什么队列?一般队列只是一个长度有限的缓冲区。如果已满,则无法保存当前任务。阻塞队列可以阻塞并保持当前需要入队的任务,但只会阻塞等待。同样,阻塞队列也可以保证当任务队列中没有任务时,会阻塞当前获取任务的线程,让其进入等待状态,释放CPU资源。所以在线程池场景下,阻塞队列其实是很有必要的。【作者简介】:公众号【秦淮杂货铺】作者秦淮,技术之路不是一时的,山高水长,纵使慢也不会停。世界希望一切都越来越快,但我更希望自己能走好每一步,把每一篇文章都写好,期待与大家交流。如果有帮助,请给我一个赞,是对我莫大的鼓励和认可。
