多线程

1      背景

早期计算机不包含操作系统,从头到尾执行一个程序,对于昂贵稀有的计算机资源是一种浪费。

要实现并发,首先需要操作系统的支持。现在的操作系统大部分都是多任务操作系统,可以“同时”执行多个任务。多任务可以在进程或线程的层面执行。

进程是指一个内存中运行的应用程序,每个进程都有自己独立的一块内存空间。多任务操作系统可以“并发”执行这些进程。
线程是指进程中乱序、多次执行的代码块,多个线程可以“同时”运行,所以认为多个线程是“并发”的。多线程的目的是为了最大限度的利用CPU资源。比如一个JVM进程中,所有的程序代码都以线程的方式运行。
这里面的“同时”、“并发”只是一种宏观上的感受,实际上从微观层面看只是进程/线程的轮换执行,只不过切换的时间非常短,所以产生了“并行”的感觉。

1.1.1       多线程vs多进程

操作系统会为每个进程分配不同的内存块,而多个线程共享进程的内存块。这带来最直接的不同就是创建线程的开销远小于创建进程的开销。
同时,由于内存块不同,所以进程之间的通信相对困难。需要采用pipe/named pipe,signal, message queue, shared memory,socket等手段;而线程间的通信简单快速,就是共享进程内的全局变量。
但是,进程的调度由操作系统负责,线程的调度就需要我们自己来考虑,避免死锁,饥饿,活锁,资源枯竭等情况的发生,这会增加一定的复杂度。而且,由于线程之间共享内存,我们还需要考虑线程安全性的问题。

 

多线程引入的问题:

  1. 安全性问题(永远不发生糟糕的事情)

Public class UnsageSequence

{

Private int value;

 

Public int getNext()

{

return value++;

}

 

}

------------------------------à>>>--------------------------------

 

A  --àvalue->9----------à9+1=10---------àvalue=10

 

B  -------------àvalue->9-------à9+1=10--------------àvalue=10

 

  1. 活跃性问题(某件正确的事情最终会发生):死锁,饥饿,活锁。
  2. 性能问题:上下文切换,服务时间过长,资源消耗过高。

 

2      多线程常用方式

2.1      继承Thread

2.2      实现Runnable

2.3      使用线程池管理

我们主需要提供任务(Runnable, Callable),线程池可以理解为一个工厂,具体负责工人人数配置以及分工。

线程池 ExecutorService的submit方法入参接受Runnable或者Callable类型的参数,Callable可以支持获取线程返回数据,会阻塞到返回对象Future的get方法,直到线程处理完毕返回数据。

线程池负责管理线程的创建和调度。

   1.当前线程数小于核心池数,每来一个任务就新建一个线程去执行。

   2.当前线程大于等于核心池数,小于线程池最大线程数,来一个任务就放到缓冲池队列,放入成功等待执行即可,若放入队列失败(任务缓存队列已满,任务执行速度低于生成任务速度),立即新建线程执行。

   3.当前线程大于线程池最大线程数,采取任务拒绝策略处理。

   4. 线程池中的线程数量大于 corePoolSize时,如果某线程空闲时间超过keepAliveTime,线程将被终止,直至线程池中的线程数目不大于corePoolSize;如果允许为核心池中的线程设置存活时间,那么核心池中的线程空闲时间超过keepAliveTime,线程也会被终止。

每个变量的作用都已经标明出来了,这里要重点解释一下corePoolSize、maximumPoolSize、largestPoolSize三个变量。

  假如有一个工厂,工厂里面有10个工人,每个工人同时只能做一件任务。

  因此只要当10个工人中有工人是空闲的,来了任务就分配给空闲的工人做;当10个工人都有任务在做时,如果还来了任务,就把任务进行排队等待;如果说新任务数目增长的速度远远大于工人做任务的速度,那么此时工厂主管可能会想补救措施,比如重新招4个临时工人进来;然后就将任务也分配给这4个临时工人做;如果说这14个工人做任务的速度还是不够,此时工厂主管可能就要考虑不再接收新的任务或者抛弃前面的一些任务了。

  当这14个工人当中有人空闲时,而新任务增长的速度又比较缓慢,工厂主管可能就考虑辞掉4个临时工了,只保持原来的10个工人,毕竟请额外的工人是要花钱的。

这个例子中的corePoolSize就是10,而maximumPoolSize就是14(10+4)。也就是说corePoolSize就是线程池大小,maximumPoolSize在我看来是线程池的一种补救措施,即任务量突然过大时的一种补救措施。

2.3.1        线程池核心处理过程(ThreadPoolExecutor)

 

 

2.3.2        Executor创建的三种类型ThreadPoolExecutor线程池

  1. FixedThreadPool

 

特点:线程数固定,队列是无界队列,maxiumPoolSize参数无效。

执行过程如下:

1.如果当前工作中的线程数量少于corePool的数量,就创建新的线程来执行任务。

2.当线程池的工作中的线程数量达到了corePool,则将任务加入LinkedBlockingQueue。

3.线程执行完1中的任务后会从队列中去任务。

注意LinkedBlockingQueue是无界队列,所以可以一直添加新任务到线程池。

 

  1. SingleThreadPool

 

特点:单个线程,其他均与FixedThreadPool相同

 

  1. CachedThreadPool

 

特点:来一个任务,看看队列中是否有空闲线程,无则新建线程。由于队列为无容量阻塞队列,每个插入操作必须等待另一个线程的对应移除操作,插入失败会新建任务。极端情况下,CachedThreadPool会因为创建过多线程而耗尽CPU资源

 

 

 

链接: https://ww.cnblogs.com/dolphin0520/p/3932921.html(结尾有源码大致分析)

Executor框架:https://www.cnblogs.com/study-everyday/archive/2017/04/20/6737428.html

posted on 2017-12-13 18:07  小付瓜  阅读(84)  评论(0)    收藏  举报