关于线程和线程池的学习,我们可以从以下几个方面入手:
第一,什么是线程,线程和进程的区别是什么
第二,线程中的基本概念,线程的生命周期
第三,单线程和多线程
第四,线程池的原理解析
第五,常见的几种线程池的特点以及各自的应用场景
一、
线程,程序执行流的最小执行单位,是行程中的实际运作单位,经常容易和进程这个概念混淆。
那么,线程和进程究竟有什么区别呢?首先,进程是一个动态的过程,是一个活动的实体。
简单来说,一个应用程序的运行就可以被看做是一个进程,而线程,是运行中的实际的任务执行者。
可以说,进程中包含了多个可以同时运行的线程。
二、
线程的生命周期,线程的生命周期可以利用以下的图解来更好的理解:
第一步,是用new Thread()的方法新建一个线程,在线程创建完成之后,线程就进入了就绪(Runnable)状态,此时创建出来的线程进入抢占CPU资源的状态,
当线程抢到了CPU的执行权之后,线程就进入了运行状态(Running),当该线程的任务执行完成之后或者是非常态的调用的stop()方法之后,线程就进入了死亡状态。
而我们在图解中可以看出,线程还具有一个阻塞的过程,这是怎么回事呢?
当面对以下几种情况的时候,容易造成线程阻塞,第一种,当线程主动调用了sleep()方法时,线程会进入则阻塞状态;
除此之外,当线程中主动调用了阻塞时的IO方法时,这个方法有一个返回参数,当参数返回之前,线程也会进入阻塞状态;
还有一种情况,当线程进入正在等待某个通知时,会进入阻塞状态。
那么,为什么会有阻塞状态出现呢?
我们都知道,CPU的资源是十分宝贵的,所以,当线程正在进行某种不确定时长的任务时,Java就会收回CPU的执行权,从而合理应用CPU的资源。
我们根据图可以看出,线程在阻塞过程结束之后,会重新进入就绪状态,重新抢夺CPU资源。
这时候,我们可能会产生一个疑问,如何跳出阻塞过程呢?
以上几种可能造成线程阻塞的情况来看,都是存在一个时间限制的,当sleep()方法的睡眠时长过去后,线程就自动跳出了阻塞状态,
第二种则是在返回了一个参数之后,在获取到了等待的通知时,就自动跳出了线程的阻塞过程。
三、
什么是单线程和多线程?
单线程,顾名思义即是只有一条线程在执行任务,这种情况在我们日常的工作学习中很少遇到,所以我们只是简单做一下了解。
多线程,创建多条线程同时执行任务,这种方式在我们的日常生活中比较常见。
但是,在多线程的使用过程中,还有许多需要我们了解的概念。
比如,在理解上并行和并发的区别,以及在实际应用的过程中多线程的安全问题,对此,我们需要进行详细的了解。
还有就是多线程的安全问题?
为什么会造成多线程的安全问题呢?
我们可以想象一下,如果多个线程同时执行一个任务,那么意味着他们共享同一种资源,由于线程CPU的资源不一定可以被谁抢占到,
这时,第一条线程先抢占到CPU资源,他刚刚进行了第一次操作,而此时第二条线程抢占到了CPU的资源,
那么,共享资源还来不及发生变化,就同时有两条数据使用了同一条资源,具体请参考多线程买票问题。这个问题我们应该如何解决那?
有造成问题的原因我们可以看出,这个问题主要的矛盾在于,CPU的使用权抢占和资源的共享发生了冲突,
解决时,我们只需要让一条线程占用了CPU的资源时,阻止第二条线程同时抢占CPU的执行权,在代码中,我们只需要在方法中使用同步代码块即可。
补充:并行与并发的区别:
理解并发、并行的例子
先举例子来理解这2个概念的区别。
老师让两个同学去办公室谈话。如果这两同学(进程)是并列跨过办公室门(CPU)的,那么就是并行。
如果同学A先进同学B后进入(或者先B后A),或者两人并列同时进入,但是在办公室外的路人甲(用户)看来,同学A和同学B同时都在办公室内,这是并发。
其实这个例子不合理,因为真正的并行是多核CPU下的概念,但上面这个简单的例子非常有助于理解。
如果举例要精确一点,那么大概是这样的:
进办公室有两个门(两CPU),如果两同学分别从不同的门进入,不管先后性,两者互相独立,那么是并行;
如果两同学不管以什么方式进入,在路人甲看来,他两同时都在办公室内,就是并发。
我不信到现在还不理解并发和并行。
并发和并行的理论性解释
为什么操作系统上可以同时运行多个程序而用户感觉不出来?
这是因为无论是单CPU还是多CPU,操作系统都营造出了可以同时运行多个程序的假象。
实际的过程操作系统对进程的调度以及CPU的快速上下文切换实现的:每个进程执行一会就先停下来,然后CPU切换到下个被操作系统调度到的进程上使之运行。
因为切换的很快,使得用户认为操作系统一直在服务自己的程序。
再来解释并发就容易理解多了。
并发(concurrent)指的是多个程序可以同时运行的现象,更细化的是多进程可以同时运行或者多指令可以同时运行。
但这不是重点,在描述并发的时候也不会去扣这种字眼是否精确,并发的重点在于它是一种现象。
并发描述的是多进程同时运行的现象。但实际上,对于单核心CPU来说,同一时刻只能运行一个进程。
所以,这里的"同时运行"表示的不是真的同一时刻有多个进程运行的现象,这是并行的概念,
而是提供一种功能让用户看来多个程序同时运行起来了,但实际上这些程序中的进程不是一直霸占CPU的,而是执行一会停一会。
所以,并发和并行的区别就很明显了。它们虽然都说是"多个进程同时运行",但是它们的"同时"不是一个概念。
并行的"同时"是同一时刻可以多个进程在运行(处于running)。
并发的"同时"是经过上下文快速切换,使得看上去多个进程同时都在运行的现象,是一种OS欺骗用户的现象。
实际上,当程序中写下多进程或多线程代码时,这意味着的是并发而不是并行。
并发是因为多进程/多线程都是需要去完成的任务,不并行是因为并行与否由操作系统的调度器决定,可能会让多个进程/线程被调度到同一个CPU核心上。
只不过调度算法会尽量让不同进程/线程使用不同的CPU核心,所以在实际使用中几乎总是会并行,但却不能以100%的角度去保证会并行。
也就是说,并行与否程序员无法控制,只能让操作系统决定。
再次注明,并发是一种现象,之所以能有这种现象的存在,和CPU的多少无关,而是和进程调度以及上下文切换有关的。
并行模式
并行意味着可以同时取得多个任务,并同时去执行所取得的这些任务。并行模式相当于将长长的一条队列,划分成了多条短队列,所以并行缩短了任务队列的长度。
正如前面所举的两同学进办公室的例子,串行的方式下,必须1个同学进入后第二个同学才进入,队列长度为2,而并行方式下可以同时进入,队列长度减半了。
并行的效率从代码层次上强依赖于多进程/多线程代码,从硬件角度上则依赖于多核CPU。
对于单进程/单线程,由于只有一个进程/线程在执行,所以尽管同时执行所取得的多个任务,但实际上这个进程/线程是不断的在多任务之间切换,一会执行一下这个,一会执行一下那个,就像是一个人在不同地方之间来回奔波。所以,单进程/线程的并行,效率比串行更低。
对于多进程/多线程,各进程/线程都可以执行各自所取得的任务,这是真正的并行。
但是,还需要考虑硬件层次上CPU核心数,如果只有单核CPU,那么在硬件角度上这单核CPU一次也只能执行一个任务,上面多进程/多线程的并行也并非真正意义上的并行。
只有多核CPU,并且多进程/多线程并行,才是真正意义上的并行。
并发
并发表示多个任务同时都要执行的现象,更详细的概念前面已经说面的够具体了。
其实,很多场景下都会使用并发的概念。比如同时500个http请求涌向了web服务器,比如有时候说并发数是1000等。
有时候也将并发当成任务,比如500并发数意味着500个任务,表示的是在一个特定的时间段内(约定俗成的大家认为是1秒)可以完成500个任务。
这500个任务可以是单进程/单线程方式处理的,这时表示的是并发不并行的模式(coroutine就是典型的并发不并行),
即先执行完一个任务后才执行另一个任务,也可以是多进程/多线程方式处理的,这时表示的是并发且并行模式。
要解决大并发问题,通常是将大任务分解成多个小任务。
很典型的一个例子是处理客户端的请求任务,这个大任务里面包含了监听并建立客户端的连接、处理客户端的请求、响应客户端。但基本上所有这类程序,都将这3部分任务分开了:在执行任何一个小任务的时候,都可以通过一些手段使得可以执行其它小任务,比如在处理请求的时候,可以继续保持监听状态。
由于操作系统对进程的调度是随机的,所以切分成多个小任务后,可能会从任一小任务处执行。这可能会出现一些现象:
-
可能出现一个小任务执行了多次,还没开始下个任务的情况。这时一般会采用队列或类似的数据结构来存放各个小任务的成果。比如负责监听的进程已经执行了多次,建立了多个连接,但是还没有调度到处理请求的进程去处理任何一个请求。
-
可能出现还没准备好第一步就执行第二步的可能。这时,一般采用多路复用或异步的方式,比如只有准备好产生了事件通知才执行某个任务。比如还没有和任何一个客户端建立连接时,就去执行了处理请求的进程。
-
可以多进程/多线程的方式并行执行这些小任务。也可以单进程/单线程执行这些小任务,这时很可能要配合多路复用才能达到较高的效率
总结
并行和串行:
- 串行:一次只能取得一个任务并执行这一个任务
- 并行:可以同时通过多进程/多线程的方式取得多个任务,并以多进程或多线程的方式同时执行这些任务
- 注意点:
- 如果是单进程/单线程的并行,那么效率比串行更差
- 如果只有单核cpu,多进程并行并没有提高效率
- 从任务队列上看,由于同时从队列中取得多个任务并执行,相当于将一个长任务队列变成了短队列
并发:
- 并发是一种现象:同时运行多个程序或多个任务需要被处理的现象
- 这些任务可能是并行执行的,也可能是串行执行的,和CPU核心数无关,是操作系统进程调度和CPU上下文切换达到的结果
- 解决大并发的一个思路是将大任务分解成多个小任务:
- 可能要使用一些数据结构来避免切分成多个小任务带来的问题
- 可以多进程/多线程并行的方式去执行这些小任务达到高效率
- 或者以单进程/单线程配合多路复用执行这些小任务来达到高效率
四,线程池
又以上介绍我们可以看出,在一个应用程序中,我们需要多次使用线程,也就意味着,我们需要多次创建并销毁线程。
而创建并销毁线程的过程势必会消耗内存。而在Java中,内存资源是及其宝贵的,所以,我们就提出了线程池的概念。
线程池:Java中开辟出了一种管理线程的概念,这个概念叫做线程池。
从概念以及应用场景中,我们可以看出,线程池的好处,就是可以方便的管理线程,也可以减少内存的消耗。
那么,我们应该如何创建一个线程池?Java中已经提供了创建线程池的一个类:Executor
而我们创建时,一般使用它的子类:ThreadPoolExecutor.
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
这是其中最重要的一个构造方法,这个方法决定了创建出来的线程池的各种属性,下面依靠一张图来更好的理解线程池和这几个参数:
图中,我们可以看出,线程池中的corePoolSize就是线程池中的核心线程数量,这几个核心线程,只是在没有用的时候,也不会被回收。
maximumPoolSize就是线程池中可以容纳的最大线程的数量,而keepAliveTime,就是线程池中除了核心线程之外的其他的最长可以保留的时间。
因为在线程池中,除了核心线程即使在无任务的情况下也不能被清除,其余的都是有存活时间的,意思就是非核心线程可以保留的最长的空闲时间,
而util,就是计算这个时间的一个单位,
workQueue,就是等待队列,任务可以储存在任务队列中等待被执行,执行的是FIFIO原则(先进先出)。
threadFactory,就是创建线程的线程工厂,最后一个handler,是一种拒绝策略,我们可以在任务满了之后,拒绝执行某些任务。
线程池的执行流程又是怎样的呢?
我们可以看出,任务进来时,首先执行判断,判断核心线程是否处于空闲状态,
如果不是,核心线程就先就执行任务,
如果核心线程已满,则判断任务队列是否有地方存放该任务,
如果有,就将任务保存在任务队列中,等待执行,
如果满了,在判断最大可容纳的线程数,
如果没有超出这个数量,就开创非核心线程执行任务,
如果超出了,就调用handler实现拒绝策略。
handler的拒绝策略:
有四种:
第一种AbortPolicy:不执行新任务,直接抛出异常,提示线程池已满
第二种DisCardPolicy:不执行新任务,也不抛出异常
第三种DisCardOldSetPolicy:将消息队列中的第一个任务替换为当前新进来的任务执行
第四种CallerRunsPolicy:直接调用execute来执行当前任务
五,四种常见的线程池:
CachedThreadPool:可缓存的线程池,该线程池中没有核心线程,非核心线程的数量为Integer.max_value,就是无限大,
当有需要时创建线程来执行任务,没有需要时回收线程,适用于耗时少,任务量大的情况。
SecudleThreadPool:周期性执行任务的线程池,按照某种特定的计划执行线程中的任务,有核心线程,但也有非核心线程,非核心线程的大小也为无限大
适用于执行周期性的任务。
SingleThreadPool:只有一条线程来执行任务,适用于有顺序的任务的应用场景。
FixedThreadPool:定长的线程池,有核心线程,核心线程的即为最大的线程数量,没有非核心线程。