线程

进程和线程的粗略对比

线程,程序执行流的最小执行单位,是行程中的实际运作单位,经常容易和进程这个概念混淆。那么,线程和进程究竟有什么区别呢?首先,进程是一个动态的过程,是一个活动的实体。简单来说,一个应用程序的运行就可以被看做是一个进程,而线程,是运行中的实际的任务执行者。可以说,进程中包含了多个可以同时运行的线程。

引入多线程技术的动机

在传统的操作系统中,进程是系统进行资源分配的基本单位,按进程为单位分给存放其映象所需要的虚地址空间、执行所需要的主存空间、完成任务需要的其他各类外围设备资源和文件。同时,进程也是处理器调度的基本单位,进程在任一时刻只有一个执行控制流,通常将这种结构的进程称单线程(结构)进程(single threaded process)。
首先来考察一个文件服务器的例子,当它接受一个文件服务请求后,由于等待磁盘传输而经常被阻塞,假如不阻塞可继续接受新的文件服务请求并进行处理,则文件服务器的性能和效率便可以提高,由于处理这些请求时要共享一个磁盘缓冲区,程序和数据,要在同一个地址空间中操作。这一类应用非常多。

例如,航空售票系统需要处理多个购票和查询请求,这些信息都与同一个数据库相关;而操作系统在同时处理许多用户进程的查询请求时,都要去访问数据库所在的同一个磁盘。

对于上述这类基于同数据区的同时多请求应用,用单线程结构的进程难以达到这一目标,即使能解决问题代价也非常高,需要寻求新概念、提出新机制。随着并行技术、网络技术和软件设计技术的发展,给并发程序设计效率带来了一系列新的问题,主要表现在:

  • 进程时空的开销大,频繁的进程调度将耗费大量处理器时间,要为每个进程分配存储空间限制了操作系统中进程的总数。
  • 进程通信的代价大,每次通信均要涉及通信进程之间或通信进程与操作系统之间的信息传递。
    进程之间的并发性粒度较粗,并发度不高,过多的进程切换和通信延迟使得细粒度的并发得不偿失。
  • 不适合并行计算和分布并行计算的要求,对于多处理器和分布式的计算环境来说,进程之间大量频繁的通信和切换,会大大降低并行度。
  • 不适合客户/服务器计算的要求。对于 C/S 结构来说,那些需要频繁输入输出并同时大量计算的服务器进程(如数据库服务器、事务监督程序)很难体现效率。

这就迫切要求操作系统改进进程结构,提供新的机制,使得应用能够按照需求在同一进程中设计出多条控制流,多控制流之间可以并行执行,多控制流切换不需通过进程调度;多控制流之间还可以通过内存区直接通信,降低通信开销。这就是近年来流行的多线程(结构)进程(multiple threaded process) 。如果说操作系统中引入进程的目的是为了使多个程序能并发执行,以改善资源使用率和提高系统效率,那么,在操作系统中再引入线程,则是为了减少程序并发执行时所付出的时空开销,使得并发粒度更细、并发性更好。这里解决问题的基本思路是:把进程的两项功能————“独立分配资源”与“被调度分派执行”分离开来,前一项任务仍由进程完成,它作为系统资源分配和保护的独立单位,不需要频繁地切换;后一项任务交给称作线程的实体来完成,它作为系统调度和分派的基本单位,会被频繁地调度和换,在这种指导思想下,产生了线程的概念。

进程和线程的本质对比

进程是系统进行资源调度和分配的基本单位;线程是CPU调度的基本单位。

进程 = 资源 (包括寄存器值,PCB,内存映射表)+ TCB(栈结构)
线程 = TCB(栈结构)

线程 的资源是共享的
进程 间的资源是分隔独立的,内存映射表不同,占用物理内存地址是分隔的

线程 的切换只是切换PC,切换了TCB(栈结构)
进程 的切换不仅要切换PC,还包括切换资源,即切换内存映射表

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

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

先是用new Thread()的方法新建一个线程,在线程创建完成之后,线程就进入了就绪(Runnable)状态,此时创建出来的线程进入抢占CPU资源的状态,当线程抢到了CPU的执行权之后,线程就进入了运行状态(Running),当该线程的任务执行完成之后或者是非常态的调用的stop()方法之后,线程就进入了死亡状态。
而我们在图解中可以看出,线程还具有一个阻塞的过程,这是怎么回事呢?当面对以下几种情况的时候,容易造成线程阻塞,第一种,当线程主动调用了sleep()方法时,线程会进入则阻塞状态,除此之外,当线程中主动调用了阻塞时的IO方法时,这个方法有一个返回参数,当参数返回之前,线程也会进入阻塞状态,还有一种情况,当线程进入正在等待某个通知时,会进入阻塞状态。那么,为什么会有阻塞状态出现呢?我们都知道,CPU的资源是十分宝贵的,所以,当线程正在进行某种不确定时长的任务时,Java就会收回CPU的执行权,从而合理应用CPU的资源。我们根据图可以看出,线程在阻塞过程结束之后,会重新进入就绪状态,重新抢夺CPU资源。这时候,我们可能会产生一个疑问,如何跳出阻塞过程呢?又以上几种可能造成线程阻塞的情况来看,都是存在一个时间限制的,当sleep()方法的睡眠时长过去后,线程就自动跳出了阻塞状态,第二种则是在返回了一个参数之后,在获取到了等待的通知时,就自动跳出了线程的阻塞过程

单线程和多线程

单线程,顾名思义即是只有一条线程在执行任务,这种情况在我们日常的工作学习中很少遇到,所以我们只是简单做一下了解

多线程,创建多条线程同时执行任务,这种方式在我们的日常生活中比较常见。但是,在多线程的使用过程中,还有许多需要我们了解的概念。比如,在理解上并行和并发的区别,以及在实际应用的过程中多线程的安全问题,对此,我们需要进行详细的了解。

并发和并行:在我们看来,都是可以同时执行多种任务,那么,到底他们二者有什么区别呢?

并发,从宏观方面来说,并行就是同时进行多种时间,实际上,这几种时间,并不是同时进行的,而是交替进行的,而由于CPU的运算速度非常的快,会造成我们的一种错觉,就是在同一时间内进行了多种事情

并行,则是真正意义上的同时进行多种事情。这种只可以在多核CPU的基础下完成。

还有就是多线程的安全问题?为什么会造成多线程的安全问题呢?我们可以想象一下,如果多个线程同时执行一个任务,name意味着他们共享同一种资源,由于线程CPU的资源不一定可以被谁抢占到,这是,第一条线程先抢占到CPU资源,他刚刚进行了第一次操作,而此时第二条线程抢占到了CPU的资源,name,共享资源还来不及发生变化,就同时有两条数据使用了同一条资源,具体请参考多线程买票问题。这个问题我们应该如何解决那?

由造成问题的原因我们可以看出,这个问题主要的矛盾在于,CPU的使用权抢占和资源的共享发生了冲突,解决时,我们只需要让一条线程战歌了CPU的资源时,阻止第二条线程同时抢占CPU的执行权,在代码中,我们只需要在方法中使用同步代码块即可。在这里,同步代码块不多进行赘述,可以自行了解。

线程池

又以上介绍我们可以看出,在一个应用程序中,我们需要多次使用线程,也就意味着,我们需要多次创建并销毁线程。而创建并销毁线程的过程势必会消耗内存。而在Java中,内存资源是及其宝贵的,所以,我们就提出了线程池的概念。

线程池:Java中开辟出了一种管理线程的概念,这个概念叫做线程池,从概念以及应用场景中,我们可以看出,线程池的好处,就是可以方便的管理线程,也可以减少内存的消耗。

那么,我们应该如何创建一个线程池那?Java中已经提供了创建线程池的一个类:Executor

而我们创建时,一般使用它的子类:ThreadPoolExecutor.

1
2
3
4
5
6
7
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:定长的线程池,有核心线程,核心线程的即为最大的线程数量,没有非核心线程

谁都是要吃饭的嘛。