Ⅰ java线程池中的核心线程是如何被重复利用的
Java线程池中的核心线程是如何被重复利用的?
引言
在Java开发中,经常需要创建线程去执行一些任务,实现起来也非常方便,但如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。此时,我们很自然会想到使用线程池来解决这个问题。
使用线程池的好处:
降低资源消耗。java中所有的池化技术都有一个好处,就是通过复用池中的对象,降低系统资源消耗。设想一下如果我们有n多个子任务需要执行,如果我们为每个子任务都创建一个执行线程,而创建线程的过程是需要一定的系统消耗的,最后肯定会拖慢整个系统的处理速度。而通过线程池我们可以做到复用线程,任务有多个,但执行任务的线程可以通过线程池来复用,这样减少了创建线程的开销,系统资源利用率得到了提升。
降低管理线程的难度。多线程环境下对线程的管理是最容易出现问题的,而线程池通过框架为我们降低了管理线程的难度。我们不用再去担心何时该销毁线程,如何最大限度的避免多线程的资源竞争。这些事情线程池都帮我们代劳了。
提升任务处理速度。线程池中长期驻留了一定数量的活线程,当任务需要执行时,我们不必先去创建线程,线程池会自己选择利用现有的活线程来处理任务。
很显然,线程池一个很显着的特征就是“长期驻留了一定数量的活线程”,避免了频繁创建线程和销毁线程的开销,那么它是如何做到的呢?我们知道一个线程只要执行完了run()方法内的代码,这个线程的使命就完成了,等待它的就是销毁。既然这是个“活线程”,自然是不能很快就销毁的。为了搞清楚这个“活线程”是如何工作的,下面通过追踪源码来看看能不能解开这个疑问。
分析方法
在分析源码之前先来思考一下要怎么去分析,源码往往是比较复杂的,如果知识储备不够丰厚,很有可能会读不下去,或者读岔了。一般来讲要时刻紧跟着自己的目标来看代码,跟目标关系不大的代码可以不理会它,一些异常的处理也可以暂不理会,先看正常的流程。就我们现在要分析的源码而言,目标就是看看线程是如何被复用的。那么对于线程池的状态的管理以及非正常状态下的处理代码就可以不理会,具体来讲,在ThreadPollExcutor类中,有一个字段private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));是对线程池的运行状态和线程池中有效线程的数量进行控制的, 它包含两部分信息: 线程池的运行状态 (runState) 和线程池内有效线程的数量 (workerCount),还有几个对ctl进行计算的方法:
以上两个方法在源码中经常用到,结合我们的目标,对运行状态的一些判断及处理可以不用去管,而对当前活动线程数要加以关注等等。
下面将遵循这些原则来分析源码。
解惑
当我们要向线程池添加一个任务时是调用ThreadPollExcutor对象的execute(Runnable command)方法来完成的,所以先来看看ThreadPollExcutor类中的execute(Runnable command)方法的源码:
按照我们在分析方法中提到的一些原则,去掉一些相关性不强的代码,看看核心代码是怎样的。
这样一看,逻辑应该清晰很多了。
如果 当前活动线程数 < 指定的核心线程数,则创建并启动一个线程来执行新提交的任务(此时新建的线程相当于核心线程);
如果 当前活动线程数 >= 指定的核心线程数,且缓存队列未满,则将任务添加到缓存队列中;
如果 当前活动线程数 >= 指定的核心线程数,且缓存队列已满,则创建并启动一个线程来执行新提交的任务(此时新建的线程相当于非核心线程);
接下来看addWorker(Runnable firstTask, boolean core)方法
同样,我们也来简化一下:
看到这里,我们大概能猜测到,addWorker方法的功能就是新建一个线程并启动这个线程,要执行的任务应该就是在这个线程中执行。为了证实我们的这种猜测需要再来看看Worker这个类。
从上面的Worker类的声明可以看到,它实现了Runnable接口,以及从它的构造方法中可以知道待执行的任务赋值给了它的变量firstTask,并以它自己为参数新建了一个线程赋值给它的变量thread,那么运行这个线程的时候其实就是执行Worker的run()方法,来看一下这个方法:
在run()方法中只调了一下 runWorker(this) 方法,再来简化一下这个 runWorker() 方法
很明显,runWorker()方法里面执行了我们新建Worker对象时传进去的待执行的任务,到这里为止貌似这个worker的run()方法就执行完了,既然执行完了那么这个线程也就没用了,只有等待虚拟机销毁了。那么回顾一下我们的目标:Java线程池中的核心线程是如何被重复利用的?好像并没有重复利用啊,新建一个线程,执行一个任务,然后就结束了,销毁了。没什么特别的啊,难道有什么地方漏掉了,被忽略了?再仔细看一下runWorker()方法的代码,有一个while循环,当执行完firstTask后task==null了,那么就会执行判断条件(task = getTask()) != null,我们假设这个条件成立的话,那么这个线程就不止只执行一个任务了,可以执行多个任务了,也就实现了重复利用了。答案呼之欲出了,接着看getTask()方法
老规矩,简化一下代码来看:
从以上代码可以看出,getTask()的作用是
如果当前活动线程数大于核心线程数,当去缓存队列中取任务的时候,如果缓存队列中没任务了,则等待keepAliveTime的时长,此时还没任务就返回null,这就意味着runWorker()方法中的while循环会被退出,其对应的线程就要销毁了,也就是线程池中少了一个线程了。因此只要线程池中的线程数大于核心线程数就会这样一个一个地销毁这些多余的线程。
如果当前活动线程数小于等于核心线程数,同样也是去缓存队列中取任务,但当缓存队列中没任务了,就会进入阻塞状态,直到能取出任务为止,因此这个线程是处于阻塞状态的,并不会因为缓存队列中没有任务了而被销毁。这样就保证了线程池有N个线程是活的,可以随时处理任务,从而达到重复利用的目的。
小结
通过以上的分析,应该算是比较清楚地解答了“线程池中的核心线程是如何被重复利用的”这个问题,同时也对线程池的实现机制有了更进一步的理解:
当有新任务来的时候,先看看当前的线程数有没有超过核心线程数,如果没超过就直接新建一个线程来执行新的任务,如果超过了就看看缓存队列有没有满,没满就将新任务放进缓存队列中,满了就新建一个线程来执行新的任务,如果线程池中的线程数已经达到了指定的最大线程数了,那就根据相应的策略拒绝任务。
当缓存队列中的任务都执行完了的时候,线程池中的线程数如果大于核心线程数,就销毁多出来的线程,直到线程池中的线程数等于核心线程数。此时这些线程就不会被销毁了,它们一直处于阻塞状态,等待新的任务到来。
注意:
本文所说的“核心线程”、“非核心线程”是一个虚拟的概念,是为了方便描述而虚拟出来的概念,在代码中并没有哪个线程被标记为“核心线程”或“非核心线程”,所有线程都是一样的,只是当线程池中的线程多于指定的核心线程数量时,会将多出来的线程销毁掉,池中只保留指定个数的线程。那些被销毁的线程是随机的,可能是第一个创建的线程,也可能是最后一个创建的线程,或其它时候创建的线程。一开始我以为会有一些线程被标记为“核心线程”,而其它的则是“非核心线程”,在销毁多余线程的时候只销毁那些“非核心线程”,而“核心线程”不被销毁。这种理解是错误的。
另外还有一个重要的接口 BlockingQueue 值得去了解,它定义了一些入队出队同步操作的方法,还可以阻塞,作用很大。
Ⅱ C语言关于队列的问题:是关于求队列的长度的,问题很简单,为了大家方便看,我把所有代码都贴出来
这个链表队列是具备头节点功能,因此在链头那里不存放数据,从后面开始算
int QueueLen(LinkQueue Q)//求队列的长度
{
int length=0;
//QnodePtr p=Q.front->next;//这是为什么是Q.front
//while(p!=NULL)//为什么这里是p!=Q.rear??? 不可以是p!=NULL?? //我的一个想法:QnodePtr p=Q.front->next; while(p!=NULL) 不知道为什么有错
//1、非循环队列判断空的依据,对头指针与对尾指针相等
//2、既然都调用DestroyQueue释放内存,后面对队列的操作没有任何意义。
//3、只要不调用DestroyQueue,你的算法也是可以实现长度计算的。
//但是一旦调用DestroyQueuefront=rear=NULL,因此front->next将指向不明地址,发生错误.
QnodePtr p=Q.front;
while(p!=Q.rear)
{
length++;
p=p->next;
}
return length;
}
Ⅲ 销毁队列的代码是啥意思解释下
typedef struct QNode{
QElemType data;
struct QNode *next;//next为指向队列中下一结点的指针
}QNode,*QueuePtr;
typedef struct{
QueuePtr front,rear;//front&rear are pointers refer to the front and the rear of the queue.
}LinkQueue;//将结构体struct定义为LinkQueue,更符合人类语言理解。
Status Destroyqueue(LinkQueue &Q) {
//队列Q存在则销毁Q
while(Q.front){//Q.front是队列的队首节点,不为NULL时执行循环
Q.rear=Q.front->next;//Q.rear指向离队首最近的节点。
free(Q.front);释放队首的节点所在内存单元
Q.front=Q.rear;现在Q.front和Q.rear都指向离原来队首最近的节点。倘若执行Destoryqueue()前节点的个数>=2,则第一次执行次循环后仍满足循环条件继续循环,最终整个队列的内存全部被释放,队列被销毁。(建议在理解的时候画张图效果较好)
}
Ⅳ java线程池怎么实现
要想理解清楚java线程池实现原理,明白下面几个问题就可以了:
(1):线程池存在哪些状态,这些状态之间是如何进行切换的呢?
(2):线程池的种类有哪些?
(3):创建线程池需要哪些参数,这些参数的具体含义是什么?
(4):将任务添加到线程池之后运行流程?
(5):线程池是怎么做到重用线程的呢?
(6):线程池的关闭
首先回答第一个问题:线程池存在哪些状态;
查看ThreadPoolExecutor源码便知晓:
[java]view plain
//runStateisstoredinthehigh-orderbits
privatestaticfinalintRUNNING=-1<<COUNT_BITS;
privatestaticfinalintSHUTDOWN=0<<COUNT_BITS;
privatestaticfinalintSTOP=1<<COUNT_BITS;
privatestaticfinalintTIDYING=2<<COUNT_BITS;
=3<<COUNT_BITS;
存在5种状态:
<1>Running:可以接受新任务,同时也可以处理阻塞队列里面的任务;
<2>Shutdown:不可以接受新任务,但是可以处理阻塞队列里面的任务;
<3>Stop:不可以接受新任务,也不处理阻塞队列里面的任务,同时还中断正在处理的任务;
<4>Tidying:属于过渡阶段,在这个阶段表示所有的任务已经执行结束了,当前线程池中是不存在有效的线程的,并且将要调用terminated方法;
<5>Terminated:终止状态,这个状态是在调用完terminated方法之后所处的状态;
那么这5种状态之间是如何进行转换的呢?查看ThreadPoolExecutor源码里面的注释便可以知道啦:
[java]view plain
*RUNNING->SHUTDOWN
*Oninvocationofshutdown(),perhapsimplicitlyinfinalize()
*(RUNNINGorSHUTDOWN)->STOP
*OninvocationofshutdownNow()
*SHUTDOWN->TIDYING
*Whenbothqueueandpoolareempty
*STOP->TIDYING
*Whenpoolisempty
*TIDYING->TERMINATED
*Whentheterminated()hookmethodhascompleted
从上面可以看到,在调用shutdown方法的时候,线程池状态会从Running转换成Shutdown;在调用shutdownNow方法的时候,线程池状态会从Running/Shutdown转换成Stop;在阻塞队列为空同时线程池为空的情况下,线程池状态会从Shutdown转换成Tidying;在线程池为空的情况下,线程池状态会从Stop转换成Tidying;当调用terminated方法之后,线程池状态会从Tidying转换成Terminate;
在明白了线程池的各个状态以及状态之间是怎么进行切换之后,我们来看看第二个问题,线程池的种类:
(1):CachedThreadPool:缓存线程池,该类线程池中线程的数量是不确定的,理论上可以达到Integer.MAX_VALUE个,这种线程池中的线程都是非核心线程,既然是非核心线程,那么就存在超时淘汰机制了,当里面的某个线程空闲时间超过了设定的超时时间的话,就会回收掉该线程;
(2):FixedThreadPool:固定线程池,这类线程池中是只存在核心线程的,对于核心线程来说,如果我们不设置allowCoreThreadTimeOut属性的话是不存在超时淘汰机制的,这类线程池中的corePoolSize的大小是等于maximumPoolSize大小的,也就是说,如果线程池中的线程都处于活动状态的话,如果有新任务到来,他是不会开辟新的工作线程来处理这些任务的,只能将这些任务放到阻塞队列里面进行等到,直到有核心线程空闲为止;
(3):ScheledThreadPool:任务线程池,这种线程池中核心线程的数量是固定的,而对于非核心线程的数量是不限制的,同时对于非核心线程是存在超时淘汰机制的,主要适用于执行定时任务或者周期性任务的场景;
(4):SingleThreadPool:单一线程池,线程池里面只有一个线程,同时也不存在非核心线程,感觉像是FixedThreadPool的特殊版本,他主要用于确保任务在同一线程中的顺序执行,有点类似于进行同步吧;
接下来我们来看第三个问题,创建线程池需要哪些参数:
同样查看ThreadPoolExecutor源码,查看创建线程池的构造函数:
[java]view plain
publicThreadPoolExecutor(intcorePoolSize,
intmaximumPoolSize,
longkeepAliveTime,
TimeUnitunit,
BlockingQueue<Runnable>workQueue,
ThreadFactorythreadFactory,
)
不管你调用的是ThreadPoolExecutor的哪个构造函数,最终都会执行到这个构造函数的,这个构造函数有7个参数,正是由于对这7个参数值的赋值不同,造成生成不同类型的线程池,比如我们常见的CachedThreadPoolExecutor、FixedThreadPoolExecutor
SingleThreadPoolExecutor、ScheledThreadPoolExecutor,我们老看看这几个参数的具体含义:
<1>corePoolSize:线程池中核心线程的数量;当提交一个任务到线程池的时候,线程池会创建一个线程来执行执行任务,即使有其他空闲的线程存在,直到线程数达到corePoolSize时不再创建,这时候会把提交的新任务放入到阻塞队列中,如果调用了线程池的preStartAllCoreThreads方法,则会在创建线程池的时候初始化出来核心线程;
<2>maximumPoolSize:线程池允许创建的最大线程数;如果阻塞队列已经满了,同时已经创建的线程数小于最大线程数的话,那么会创建新的线程来处理阻塞队列中的任务;
<3>keepAliveTime:线程活动保持时间,指的是工作线程空闲之后继续存活的时间,默认情况下,这个参数只有线程数大于corePoolSize的时候才会起作用,即当线程池中的线程数目大于corePoolSize的时候,如果某一个线程的空闲时间达到keepAliveTime,那么这个线程是会被终止的,直到线程池中的线程数目不大于corePoolSize;如果调用allowCoreThreadTimeOut的话,在线程池中线程数量不大于corePoolSize的时候,keepAliveTime参数也可以起作用的,知道线程数目为0为止;
<4>unit:参数keepAliveTime的时间单位;
<5>workQueue:阻塞队列;用于存储等待执行的任务,有四种阻塞队列类型,ArrayBlockingQueue(基于数组的有界阻塞队列)、LinkedBlockingQueue(基于链表结构的阻塞队列)、SynchronousQueue(不存储元素的阻塞队列)、PriorityBlockingQueue(具有优先级的阻塞队列);
<6>threadFactory:用于创建线程的线程工厂;
<7>handler:当阻塞队列满了,且没有空闲线程的情况下,也就是说这个时候,线程池中的线程数目已经达到了最大线程数量,处于饱和状态,那么必须采取一种策略来处理新提交的任务,我们可以自己定义处理策略,也可以使用系统已经提供给我们的策略,先来看看系统为我们提供的4种策略,AbortPolicy(直接抛出异常)、CallerRunsPolicy(只有调用者所在的线程来运行任务)、DiscardOldestPolicy(丢弃阻塞队列中最近的一个任务,并执行当前任务)、Discard(直接丢弃);
接下来就是将任务添加到线程池之后的运行流程了;
我们可以调用submit或者execute方法,两者最大的区别在于,调用submit方法的话,我们可以传入一个实现Callable接口的对象,进而能在当前任务执行结束之后通过Future对象获得任务的返回值,submit内部实际上还是执行的execute方法;而调用execute方法的话,是不能获得任务执行结束之后的返回值的;此外,调用submit方法的话是可以抛出异常的,但是调用execute方法的话,异常在其内部得到了消化,也就是说异常在其内部得到了处理,不会向外传递的;
因为submit方法最终也是会执行execute方法的,因此我们只需要了解execute方法就可以了:
在execute方法内部会分三种情况来进行处理:
<1>:首先判断当前线程池中的线程数量是否小于corePoolSize,如果小于的话,则直接通过addWorker方法创建一个新的Worker对象来执行我们当前的任务;
<2>:如果说当前线程池中的线程数量大于corePoolSize的话,那么会尝试将当前任务添加到阻塞队列中,然后第二次检查线程池的状态,如果线程池不在Running状态的话,会将刚刚添加到阻塞队列中的任务移出,同时拒绝当前任务请求;如果第二次检查发现当前线程池处于Running状态的话,那么会查看当前线程池中的工作线程数量是否为0,如果为0的话,就会通过addWorker方法创建一个Worker对象出来处理阻塞队列中的任务;
<3>:如果原先线程池就不处于Running状态或者我们刚刚将当前任务添加到阻塞队列的时候出现错误的话,那么会去尝试通过addWorker创建新的Worker来处理当前任务,如果添加失败的话,则拒绝当前任务请求;
可以看到在上面的execute方法中,我们仅仅只是检查了当前线程池中的线程数量有没有超过corePoolSize的情况,那么当前线程池中的线程数量有没有超过maximumPoolSize是在哪里检测的呢?实际上是在addWorker方法里面了,我们可以看下addWorker里面的一段代码:
[java]view plain
if(wc>=CAPACITY||
wc>=(core?corePoolSize:maximumPoolSize))
returnfalse;
如果当前线程数量超过maximumPoolSize的话,直接就会调用return方法,返回false;
其实到这里我们很明显可以知道,一个线程池中线程的数量实际上就是这个线程池中Worker的数量,如果Worker的大小超过了corePoolSize,那么任务都在阻塞队列里面了,Worker是Java对我们任务的一个封装类,他的声明是酱紫的:
[java]view plain
privatefinalclassWorker
implementsRunnable
可以看到他实现了Runnable接口,他是在addWorker方法里面通过new Worker(firstTask)创建的,我们来看看他的构造函数就知道了:
[java]view plain
Worker(RunnablefirstTask){
setState(-1);//
this.firstTask=firstTask;
this.thread=getThreadFactory().newThread(this);
}
而这里的firstTask其实就是我们调用execute或者submit的时候传入的那个参数罢了,一般来说这些参数是实现Callable或者Runnable接口的;
在通过addWorker方法创建出来Worker对象之后,这个方法的最后会执行Worker内部thread属性的start方法,而这个thread属性实际上就是封装了Worker的Thread,执行他的start方法实际上执行的是Worker的run方法,因为Worker是实现了Runnable接口的,在run方法里面就会执行runWorker方法,而runWorker方法里面首先会判断当前我们传入的任务是否为空,不为空的话直接就会执行我们通过execute或者submit方法提交的任务啦,注意一点就是我们虽然会通过submit方法提交实现了Callable接口的对象,但是在调用submit方法的时候,其实是会将Callable对象封装成实现了Runnable接口对象的,不信我们看看submit方法源码是怎么实现的:
[java]view plain
public<T>Future<T>submit(Callable<T>task){
if(task==null)thrownewNullPointerException();
RunnableFuture<T>ftask=newTaskFor(task);
execute(ftask);
returnftask;
}
看到没有呢,实际上在你传入实现了Callable接口对象的时候,在submit方法里面是会将其封装成RunnableFuture对象的,而RunnableFuture接口是继承了Runnable接口的;那么说白了其实就是直接执行我们提交任务的run方法了;如果为空的话,则会通过getTask方法从阻塞队列里面拿出一个任务去执行;在任务执行结束之后继续从阻塞队列里面拿任务,直到getTask的返回值为空则退出runWorker内部循环,那么什么情况下getTask返回为空呢?查看getTask方法的源码注释可以知道:在Worker必须需要退出的情况下getTask会返回空,具体什么情况下Worker会退出呢?(1):当Worker的数量超过maximumPoolSize的时候;(2):当线程池状态为Stop的时候;(3):当线程池状态为Shutdown并且阻塞队列为空的时候;(4):使用等待超时时间从阻塞队列中拿数据,但是超时之后仍然没有拿到数据;
如果runWorker方法退出了它里面的循环,那么就说明当前阻塞队列里面是没有任务可以执行的了,你可以看到在runWorker方法内部的finally语句块中执行了processWorkerExit方法,用来对Worker对象进行回收操作,这个方法会传入一个参数表示需要删除的Worker对象;在进行Worker回收的时候会调用tryTerminate方法来尝试关闭线程池,在tryTerminate方法里面会检查是否有Worker在工作,检查线程池的状态,没问题的话就会将当前线程池的状态过渡到Tidying,之后调用terminated方法,将线程池状态更新到Terminated;
从上面的分析中,我们可以看出线程池运行的4个阶段:
(1):poolSize < corePoolSize,则直接创建新的线程(核心线程)来执行当前提交的任务;
(2):poolSize = corePoolSize,并且此时阻塞队列没有满,那么会将当前任务添加到阻塞队列中,如果此时存在工作线程(非核心线程)的话,那么会由工作线程来处理该阻塞队列中的任务,如果此时工作线程数量为0的话,那么会创建一个工作线程(非核心线程)出来;
(3):poolSize = corePoolSize,并且此时阻塞队列已经满了,那么会直接创建新的工作线程(非核心线程)来处理阻塞队列中的任务;
(4):poolSize = maximumPoolSize,并且此时阻塞队列也满了的话,那么会触发拒绝机制,具体决绝策略采用的是什么就要看我们创建ThreadPoolExecutor的时候传入的RejectExecutionHandler参数了;
接下来就是线程池是怎么做到重用线程的呢?
个人认为线程池里面重用线程的工作是在getTask里面实现的,在getTask里面是存在两个for死循环嵌套的,他会不断的从阻塞对列里面取出需要执行的任务,返回给我们的runWorker方法里面,而在runWorker方法里面只要getTask返回的任务不是空就会执行该任务的run方法来处理它,这样一直执行下去,直到getTask返回空为止,此时的情况就是阻塞队列里面没有任务了,这样一个线程处理完一个任务之后接着再处理阻塞队列中的另一个任务,当然在线程池中的不同线程是可以并发处理阻塞队列中的任务的,最后在阻塞队列内部不存在任务的时候会去判断是否需要回收Worker对象,其实Worker对象的个数就是线程池中线程的个数,至于什么情况才需要回收,上面已经说了,就是四种情况了;
最后就是线程池是怎样被关闭的呢?
涉及到线程池的关闭,需要用到两个方法,shutdown和shutdownNow,他们都是位于ThreadPoolExecutor里面的,对于shutdown的话,他会将线程池状态切换成Shutdown,此时是不会影响对阻塞队列中任务执行的,但是会拒绝执行新加进来的任务,同时会回收闲置的Worker;而shutdownNow方法会将线程池状态切换成Stop,此时既不会再去处理阻塞队列里面的任务,也不会去处理新加进来的任务,同时会回收所有Worker;
Ⅳ 队列的源代码(c语言)
以前写的.你可以看看咯..
class Queue
{
struct Node
{
int a;
Node * next;
};
public:
Queue();
void pump(int b);
void pop();
int getlength();
virtual void print();
private:
Node * head;
Node * rear;
};
void Queue::pump(int b)
{
Node *p1=new Node;
p1->a=b;
p1->next=NULL;
rear->next=p1;
rear=p1;
head->a++;
cout<<setw(2)<<b<<setw(2)<<" 进入队列 "<<endl;
}
void Queue::pop()
{
Node *p;
p=head->next;
cout<<" "<<setw(2)<<p->a<<setw(2)<<"出队"<<endl;
head->next=p->next;
delete p;
head->a--;
}
int Queue::getlength()
{
return head->a;
}
void Queue::print()
{
Node *p;
p=head->next;
cout<<"队列中的元素是:"<<endl;
while(p)
{
cout<<p->a<<" -> ";
p=p->next;
}
cout<<"NULL"<<endl;
}
Queue::Queue()
{
rear=head;
};
Ⅵ java 线程池 工作队列是如何工作的
使用线程池的好处
1、降低资源消耗
可以重复利用已创建的线程降低线程创建和销毁造成的消耗。
2、提高响应速度
当任务到达时,任务可以不需要等到线程创建就能立即执行。
3、提高线程的可管理性
线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控
线程池的工作原理
首先我们看下当一个新的任务提交到线程池之后,线程池是如何处理的
1、线程池判断核心线程池里的线程是否都在执行任务。如果不是,则创建一个新的工作线程来执行任务。如果核心线程池里的线程都在执行任务,则执行第二步。
2、线程池判断工作队列是否已经满。如果工作队列没有满,则将新提交的任务存储在这个工作队列里进行等待。如果工作队列满了,则执行第三步
3、线程池判断线程池的线程是否都处于工作状态。如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务
线程池饱和策略
这里提到了线程池的饱和策略,那我们就简单介绍下有哪些饱和策略:
AbortPolicy
为Java线程池默认的阻塞策略,不执行此任务,而且直接抛出一个运行时异常,切记ThreadPoolExecutor.execute需要try catch,否则程序会直接退出。
DiscardPolicy
直接抛弃,任务不执行,空方法
DiscardOldestPolicy
从队列里面抛弃head的一个任务,并再次execute 此task。
CallerRunsPolicy
在调用execute的线程里面执行此command,会阻塞入口
用户自定义拒绝策略(最常用)
实现RejectedExecutionHandler,并自己定义策略模式
下我们以ThreadPoolExecutor为例展示下线程池的工作流程图
3.jpg
关键方法源码分析
我们看看核心方法添加到线程池方法execute的源码如下:
// //Executes the given task sometime in the future. The task //may execute in a new thread or in an existing pooled thread. // // If the task cannot be submitted for execution, either because this // executor has been shutdown or because its capacity has been reached, // the task is handled by the current {@code RejectedExecutionHandler}. // // @param command the task to execute // @throws RejectedExecutionException at discretion of // {@code RejectedExecutionHandler}, if the task // cannot be accepted for execution // @throws NullPointerException if {@code command} is null // public void execute(Runnable command) { if (command == null) throw new NullPointerException(); // // Proceed in 3 steps: // // 1. If fewer than corePoolSize threads are running, try to // start a new thread with the given command as its first // task. The call to addWorker atomically checks runState and // workerCount, and so prevents false alarms that would add // threads when it shouldn't, by returning false. // 翻译如下: // 判断当前的线程数是否小于corePoolSize如果是,使用入参任务通过addWord方法创建一个新的线程, // 如果能完成新线程创建exexute方法结束,成功提交任务 // 2. If a task can be successfully queued, then we still need // to double-check whether we should have added a thread // (because existing ones died since last checking) or that // the pool shut down since entry into this method. So we // recheck state and if necessary roll back the enqueuing if // stopped, or start a new thread if there are none. // 翻译如下: // 在第一步没有完成任务提交;状态为运行并且能否成功加入任务到工作队列后,再进行一次check,如果状态 // 在任务加入队列后变为了非运行(有可能是在执行到这里线程池shutdown了),非运行状态下当然是需要 // reject;然后再判断当前线程数是否为0(有可能这个时候线程数变为了0),如是,新增一个线程; // 3. If we cannot queue task, then we try to add a new // thread. If it fails, we know we are shut down or saturated // and so reject the task. // 翻译如下: // 如果不能加入任务到工作队列,将尝试使用任务新增一个线程,如果失败,则是线程池已经shutdown或者线程池 // 已经达到饱和状态,所以reject这个他任务 // int c = ctl.get(); // 工作线程数小于核心线程数 if (workerCountOf(c) < corePoolSize) { // 直接启动新线程,true表示会再次检查workerCount是否小于corePoolSize if (addWorker(command, true)) return; c = ctl.get(); } // 如果工作线程数大于等于核心线程数 // 线程的的状态未RUNNING并且队列notfull if (isRunning(c) && workQueue.offer(command)) { // 再次检查线程的运行状态,如果不是RUNNING直接从队列中移除 int recheck = ctl.get(); if (! isRunning(recheck) && remove(command)) // 移除成功,拒绝该非运行的任务 reject(command); else if (workerCountOf(recheck) == 0) // 防止了SHUTDOWN状态下没有活动线程了,但是队列里还有任务没执行这种特殊情况。 // 添加一个null任务是因为SHUTDOWN状态下,线程池不再接受新任务 addWorker(null, false); } // 如果队列满了或者是非运行的任务都拒绝执行 else if (!addWorker(command, false)) reject(command); }
Ⅶ 如何:清除队列内容
可以使用 Purge 方法清除“消息队列”系统中您有权访问的任何队列的内容。例如,假设在本地“消息队列”客户端上使用日记队列记录送出的所有消息的副本。当日记达到其大小上限时,您可以使用 Purge 方法清除不再需要的项。说明:存储在日记和死信队列中的消息计数以队列所在计算机的配额为限。当到达此配额时,这些队列停止接受新消息。如果在应用程序中使用日记和死信队列,定期从这些队列中清除不再需要的消息很重要。若要删除队列的内容,必须在“消息队列”中具有接收权限。清除队列时,它包含的所有消息不发送即被丢弃。可以从代码或“服务器资源管理器”中清除队列的内容。从“服务器资源管理器”中清除队列的内容打开“服务器资源管理器”,然后展开要清除的队列所在的服务器节点。提示:如果“服务器”节点中未列出所需的服务器,请右击“服务器”,单击“添加服务器”,输入服务器的名称,然后单击“确定”。展开“消息队列”节点。展开要删除其中的消息的队列。您将看到该队列的消息及其关联日记消息的一个或多个节点。右击要清除的消息节点,然后单击“清除消息”。以编程方式清除队列的内容使用 Purge 方法可以清除适当队列的内容。代码应类似于:Dim MessageQueue1 As New System.Messaging.MessageQueue() MessageQueue1.Path = ".\MyQueue" MessageQueue1.Purge() System.Messaging.MessageQueue MessageQueue1 = new System.Messaging.MessageQueue(); MessageQueue1.Path = @".\MyQueue"; MessageQueue1.Purge(); 此代码示例也可用作 IntelliSense 代码段。在代码段选择器中,此代码示例位于“Windows 操作系统”>“消息队列”中。请参见任务 如何:创建队列其他资源 创建、删除和管理队列
Ⅷ 循环队列的判满操作的核心代码是什么
咨询记录 · 回答于2021-10-19
Ⅸ C关于队列的问题:是关于求队列的长度的,问题很简单,为了大家方便看,我把所有代码都贴出来
#include <stdio.h>
#include <conio.h>
#include <math.h>
#include <stdlib.h>
#define YES 1
#define NO 0
typedef struct qnode
{
int data;
struct qnode *next;
}*QnodePtr,Qnode;
typedef struct linkqueue
{
QnodePtr front;
QnodePtr rear;
}LinkQueue;
void InitQueue(LinkQueue *Q)//初始化队列
{
if(!(Q->front=Q->rear=(QnodePtr)malloc(sizeof(Qnode))))
exit(OVERFLOW);
Q->front->next=NULL;
}
void DestroyQueue(LinkQueue *Q)//销毁一个队列
{
while(Q->front!=NULL)
{
Q->rear=Q->front->next;
free(Q->front);
Q->front=Q->rear;
}
Q=NULL;
}
void ClearQueue(LinkQueue *Q)//清空一个队列
{
QnodePtr p=NULL,q=NULL;
Q->rear=Q->front;
p=Q->front->next;
Q->front->next=NULL;
while(p)
{
q=p;
p=p->next;
free(q);
}
}
int QueueEmpty(LinkQueue Q)//判断队列是否为空
{
if(Q.front==Q.rear)
return YES;
return NO;
}
int GetHead(LinkQueue Q,int *e)//得到队首元素
{
QnodePtr p=Q.front->next;
if(Q.front==Q.rear)
return NO;
*e=p->data;
return YES;
}
int QueueLen(LinkQueue Q)//求队列的长度
{
if (Q.front==NULL)//这里加一条语句,假如队列的头结点不存在,那么它的长度自然是0,如果头结点都不存在了,你还要去访问它的下一个结点,那么程序自然就崩溃了
{
return 0;
}
int length=0;
QnodePtr p=Q.front->next;//这是为什么是Q.front
while(p!=NULL)//为什么这里是p!=Q.rear??? 不可以是p!=NULL?? //我的一个想法:QnodePtr p=Q.front->next; while(p!=NULL) 不知道为什么有错
{
length++;
p=p->next;
}
return length;
}
void QueueInsert(LinkQueue *Q,int e)//向队尾插入一个元素e
{
QnodePtr p=(QnodePtr)malloc(sizeof(Qnode));
if(!p)
exit(OVERFLOW);
p->data=e;
p->next=NULL;
Q->rear->next=p;
Q->rear=p;
}
int QueueDelete(LinkQueue *Q,int *e)//从队首删除一个元素
{
QnodePtr p=NULL;
if(Q->front==Q->rear)
return NO;
p=Q->front->next;
*e=p->data;
Q->front->next=p->next;
if(p==Q->rear)
Q->rear=Q->front;
free(p);
return YES;
}
void QueueTraverse(LinkQueue Q,void (*vi)(int))//对队列中的每个元素都调用vi函数
{
QnodePtr p=Q.front->next;
while(p)
{
vi(p->data);
p=p->next;
}
}
void Print(int i)
{
printf("%d ",i);
}
int main()
{
LinkQueue Q;
int i;
InitQueue(&Q);
QueueInsert(&Q,10);
QueueInsert(&Q,20);
QueueInsert(&Q,30);
QueueTraverse(Q,Print);
QueueTraverse(Q,Print);
printf("len:\n\na%d\n",QueueLen(Q));
GetHead(Q,&i);
printf("\n\n%d\n",i);
printf("\n\nb%d",QueueEmpty(Q));
DestroyQueue(&Q);
printf("\n\n%d",QueueLen(Q));
getch();
}
要求长度的时候,先要判断头结点是否存在。。。
Ⅹ 给个c语言写的队列。源码,最好能在linux下运行的。
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#define BUF_SIZE 50 //队列中存储字符串的个数
struct node
{
char buf[BUF_SIZE];
struct node *next;
};
//入队(队列带头结点)
void push_value(char *value,struct node *head)
{
struct node *p=head;
while(p->next!=NULL) //查找最后一个元素
{
p=p->next;
}
struct node *new_node=(struct node*)malloc(sizeof(struct node));
strncpy(new_node->buf,value,BUF_SIZE);
new_node->next=NULL;
p->next=new_node; //在队列尾部插入一个新的元素
}
//出队,取队列头部第一个元素
char *pop_value(struct node *head)
{
struct node *p=head->next; //取第一个元素
head->next=p->next; //删除队列中第一个元素
char *result=(char *)malloc(BUF_SIZE*sizeof(char));
strncpy(result,p->buf,BUF_SIZE);
return result;
}
//输出元素
void print_queue(struct node *head)
{
struct node *p=head->next;
while(p!=NULL)
{
printf("%s\n",p->buf);
p=p->next;
}
}
int main()
{
//带头结点的队列
struct node *head=(struct node*)malloc(sizeof(struct node));
head->next=NULL;
push_value("AAA",head);
push_value("BBB",head);
push_value("CCC",head);
print_queue(head);
char *value=pop_value(head);
printf("pop value is:%s\n",value);
printf("after pop...\n");
print_queue(head);
return 0;
}