线程和线程池理解
关于线程和线程池的学习,我们可以从以下几个方面入手:
第一,什么是线程,线程和进程的区别是什么
第二,线程中的基本概念,线程的生命周期
第三,单线程和多线程
第四,线程池的原理解析
第五,常见的几种线程池的特点以及各自的应用场景
一、什么是线程,线程和进程的区别是什么
首先,进程是一个动态的过程,是一个活动的实体,简单来说,一个应用程序的运行就可以看作是一个进程。而线程,是运行中实际任务的执行者。可以说,进程中包含了多个可以同时运行的线程。
二、什么是线程的生命周期
第一阶段新建,用new Thread) 方法创建一个新线程。
第二阶段就绪,调用线程的start) 方法,线程进入就绪Runnable)状态,此时创建出来的线程进入抢占CPU资源的状态,谁先抢到CPU资源,谁先开始执行。
第三阶段运行,当线程抢到了CPU的执行权后,线程就进入了运行(Running)状态,此时会执行run)方法来实现线程的操作和功能。
第四阶段阻塞,在运行过程中,可能因为某些原因导致运行状态的线程进入阻塞状态,第一种,当线程主动调用了sleep)状态时,线程会进入阻塞状态,当睡眠时常过去后,就会自动进入就绪状态。还有一种当线程进入正在等待wait)某个通知,会进入阻塞状态,直到线程获取notify()/notifyAll()消息,线程才会进入就绪状态。第三种线程在等待某个输入输出流的完成。第四种在某个对象调用synchronized同步控制。那么,为什么会有阻塞状态出现呢?我们都知道,CPU的资源是十分宝贵的,所以,当线程正在进行某种不确定时长的任务时,Java就会收回CPU的执行权,从而合理应用CPU的资源。
第五阶段销毁,如果线程正常执行完毕,或非常态的调用stop)方法强制性中止,或出现异常导致结束,那么线程就要被销毁释放资源。
三、什么是单线程和多线程
单线程顾名思义是只有一条线程在执行任务,在工作中很难遇到。多线程则是创建多条线程同时执行任务,在多线程的使用过程中,还有许多需要我们了解的概念。比如,在理解上串行,并行和并发的区别,以及在实际应用的过程中多线程的安全问题,对此,我们需要进行详细的了解。
串行,并行,并发:
串行:多个任务,执行时一个执行完再执行另一个。 比喻:吃完饭再看视频。
并发:多个线程在单个核心运行,同一时间一个线程运行,系统不停的切换线程,看起来是同时运行,实际上是系统不停的切换。比喻:一会跑去吃饭,一会去看电视。
并行:每个线程分配独立的核心,线程同时运行。比喻:一边吃饭一边看电视。
在单CPU系统中,系统调度在某一时刻只能让一个线程运行,调度机制有多种形式,一般是以时间片轮询的方式。这种不断切换需要运行的线程让其运行就叫并发(concurrent)。而在多CPU系统中,可以让两个以上的线程同时运行,这就叫作并行(parallel)。
并发是有状态的,“具有可论证的确定性,但是实际上具有不可确定性”;”并发”在微观上不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行,从宏观外来看,好像是这些进程都在执行。使用多个线程可以帮助我们在单个处理系统中实现更高的吞吐量,如果一个程序是单线程的,这个处理器在等待一个同步I/O操作完成的时候,他仍然是空闲的。在多线程系统中,当一个线程等待I/O的同时,其他的线程也可以执行。
异步与多线程:
异步和多线程并不是同一关系,异步是最终目的,多线程只是我们实现异步的一种手段。异步是调用者发送一个请求给被调用者,而调用者不用等待请求结果的返回,可以去做其他事。实现异步可以使用多线程或交给其他进程来处理。
1)多线程实现异步的好处,因为异步代码比较难以实现,多线程相对容易,但多线程本质是同步,效率上比不上异步。
2)多线程和异步都可以实现避免线程堵塞的问题,从而提高软件的可响应性。
3)异步操作的本质:所有的程序最终都会由计算机硬件来执行,所以为了更好的理解异步操作的本质,我们有必要了解一下它的硬件基础。 熟悉电脑硬件的朋友肯定对DMA这个词不陌生,硬盘、光驱的技术规格中都有明确DMA的模式指标,其实网卡、声卡、显卡也是有DMA功能的。DMA就是直接内存访问的意思,也就是说,拥有DMA功能的硬件在和内存进行数据交换的时候可以不消耗CPU资源。只要CPU在发起数据传输时发送一个指令,硬件就开始自己和内存交换数据,在传输完成之后硬件会触发一个中断来通知操作完成。这些无须消耗CPU时间的I/O操作正是异步操作的硬件基础。所以即使在DOS这样的单进程(而且无线程概念)系统中也同样可以发起异步的DMA操作。
4)线程的本质:线程不是一个计算机硬件的功能,二十操作系统提供的一种逻辑功能,线程的本质是进程中一段并发运行的代码,所以线程需要操作系统提供CPU资源来进行运行和调度。
5)异步操作的优缺点:因为异步操作无需额外的线程负担,并且使用回调的方式进行处理,在设计良好的情况下,处理函数可以不用使用共享变量(即使无法完全不用,最起码可以减少共享变量的数量),减少死锁的可能性。当然异步操作也并非完美无暇。编写异步操作的复杂程度较高,程序主要使用回调方式进行处理,与普通人的思维方式有些初入,而且难以调试。
6)多线程的优缺点:多线程的优点很明显,线程中的处理程序依然是顺序执行,符合普通人的思维习惯,所以编程简单。但是多线程的缺点也同样明显,线程的使用(滥用)会给系统带来上下文切换的额外负担。并且线程间的共享变量可能造成死锁的出现。
7)适用范围:我认为:当需要执行I/O操作时,使用异步操作比使用线程+同步I/O操作更合适。I/O操作不仅包括了直接的文件、网络的读写,还包括数据库操作、Web Service、HttpRequest以及.Net Remoting等跨进程的调用。而线程的适用范围则是那种需要长时间CPU运算的场合,例如耗时较长的图形处理和算法执行。但是往往由于使用线程编程的简单和符合习惯,所以很多朋友往往会使用线程来执行耗时较长的I/O操作。这样在只有少数几个并发操作的时候还无伤大雅,如果需要处理大量的并发操作时就不合适了。
多核下线程数量选择
计算密集型
程序主要为复杂的逻辑判断和复杂的运算。
cpu的利用率高,不用开太多的线程,开太多线程反而会因为线程切换时切换上下文而浪费资源。
IO密集型
程序主要为IO操作,比如磁盘IO读取文件)和网络IO网络请求)。
因为IO操作会阻塞线程,cpu利用率不高,可以开多点线程,阻塞时可以切换到其他就绪线程,提高cpu利用率。
总结
提高性能的一种方式:提高硬件水平,处理速度或核心数。
另一种方式:根据实际场景,合理设置线程数,软件上提高cpu利用率。
四、线程池的原理解析
从以上介绍我们可以看出在一个应用程序中,我们需要多次使用线程,也就意味着,我们需要多次创建和销毁线程。而创建和销毁线程的过程势必消耗内存,在Java中内存资源是十分宝贵的,因此我们提出线程池的概念。
线程池:线程池是一种多线程的处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。
如何创建线程池?Java中提供了创建线程池的一个类Executor
而我们创建时一般使用其子类ThreadPoolExecutor。
public ThreadPoolExecutorint corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)
这是其中最重要的一个构造方法,这个方法决定了创建出来的线程池的各种属性,下面依靠一张图来更好的理解线程池和这几个参数:
从图中我们可以看出,线程池中的corePoolSize就是线程池中的核心线程数,这几个核心线程,在没有用时也不会被回收。maximumPoolSize就是线程池中可容纳的最大线程数,而KeepAliveTime就是线程池除核心线程外其他线程最长可保留时间,因为在线程池中,除了核心线程其他线程都有存活时间能被清除。Util就是计算这个时间的单位。workQueue就是等待队列,任务可以存储在任务队列中等待被执行,执行是先进先出原则。threadFactory这是创建线程的线程工厂,最后一个handler是拒绝策略,我们可以在任务满了之后,拒绝执行某些任务。
线程池的执行流程又是怎样的呢?
由图我们可以看出,任务进来时,首先执行判断,判断核心线程是否处于空闲状态,如果不是,核心线程就先就执行任务,如果核心线程已满,则判断任务队列是否有地方存放该任务,若果有,就将任务保存在任务队列中,等待执行,如果满了,在判断最大可容纳的线程数,如果没有超出这个数量,就开创非核心线程执行任务,如果超出了,就调用handler实现拒绝策略。
handler的拒绝策略:
有四种:
1. AbortPolicy: 不执行任务,直接抛出异常,提示线程池已满
2. DisCardPolicy: 不执行新任务,也不抛出异常
3. DisCardOldSetPolicy: 将消息队列中的第一个任务替换替换为当前新进来的任务执行
4. CallerRunPolicy: 直接调用execute来执行当前任务
五、四种常见的线程池
1. CachedThreadPool: 可缓存的线程池,该线程池中没有核心线程,非核心线程的数量为Integer.max_value,就是无限大,当有需要时创建线程来执行任务,没有需要时回收线程,适用于耗时少,任务量大的情况。
2. ScheduledThreadPool: 周期性执行任务的线程池,按照某种特定的计划执行线程中的任务,有核心线程,但也有非核心线程,非核心线程的大小也为无限大。适用于执行周期性的任务。
3. SingleThreadPool: 只有一条线程来执行任务,适用于有顺序的任务的应用场景。
4. FixedThreadPool: 定长的线程池,有核心线程,核心线程即为最大的线程数量,没有非核心线程。当前的线程数能够比较稳定保证一个数。能够避免频繁回收线程和创建线程。故适用于处理cpu密集型的任务,确保cpu在长期被工作线程使用的情况下,尽可能少的分配线程,即适用长期的任务。