• 测试开发进阶——常用中间件概念——线程与线程池理解


    关于线程和线程池的学习,我们可以从以下几个方面入手:

    第一,什么是线程,线程和进程的区别是什么

    第二,线程中的基本概念,线程的生命周期

    第三,单线程和多线程

    第四,线程池的原理解析

    第五,常见的几种线程池的特点以及各自的应用场景

    一、

    线程,程序执行流的最小执行单位,是行程中的实际运作单位,经常容易和进程这个概念混淆。

    那么,线程和进程究竟有什么区别呢?首先,进程是一个动态的过程,是一个活动的实体。

    简单来说,一个应用程序的运行就可以被看做是一个进程,而线程,是运行中的实际的任务执行者。

    可以说,进程中包含了多个可以同时运行的线程。

    二、

    线程的生命周期,线程的生命周期可以利用以下的图解来更好的理解:


    第一步,是用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:定长的线程池,有核心线程,核心线程的即为最大的线程数量,没有非核心线程。

  • 相关阅读:
    【贪心】闭区间问题
    【贪心】电视节目安排
    中石油-高精度阶乘-java
    hdu 6444 Neko's loop 单调队列优化DP
    hdu 3415 Max Sum of Max-K-sub-sequence 单调队列优化DP
    51nod 1050 循环数组最大子段和 单调队列优化DP
    hdu 6406 Taotao Picks Apples 线段树 单点更新
    Wannafly 挑战赛22 D 整数序列 线段树 区间更新,区间查询
    dp专题训练
    zoj 3747 递推dp
  • 原文地址:https://www.cnblogs.com/xiaobaibailongma/p/15333935.html
Copyright © 2020-2023  润新知