任务并行的实质是同时可以运行的线程数量。
jdk concurrent future callable,要写匿名块在callable内完成业务逻辑,future.get方法会过早的阻塞调用线程,还要结合future. is Done取得 子线程的结果,否则就是轮询取得结果。这只能在调用线程处理一个任务,当有多个任务并行时代码块较多,子线程的处理结果合并到调 用线程、或子线程的异常在调用线程难以处理,还会有callback hell的问题。
以前主流的CPU都是单核,CPU的核心频率是运行程序性能重要指标。当日刘ava比较流行的是单线程编程,对于CPU密集型的应用程序而 言,频繁的通过多线程进行协作和抢占时间片反而会降低性能。
现在的CPU基本上是多核,每核多硬件线程。比如我这台电脑是2核,2硬件线程。在一个CPU时钟周期内,每核2硬件线程中只有一个可 以运行。当CPU发生长延迟时,如果同一个核中还有其他就绪的硬件线程,下一个时钟周期就会让这个硬件运行。相比,每核单硬件线程 的CPU,就会被长延迟事件所阻塞,因为等待事件完成浪费时钟周期。对于这类CPU,如果就绪的应用线程已经准备好运行却没有可用的 硬件线程,运行前就必须进行线程上下文切换,线程上下文切换通常要消耗数百个时钟周期。而每核多硬件线程的CPU可以在下一个时钟 周期切换到同核上的另一个就绪线程,对于有许多执行线程的应用,每核多硬件线程的CPU会执行的更快。
线程模型的发展过程:
Screen Shot 2020-06-17 at 11.05.46 AM.png・单线程处理所有请求,n个请求:1个线程,只有在完全处理好一个请求后才能接收下一个请求,一个请求blocking,会导致后续请求无法处理。
Screen Shot 2020-06-17 at 11.08.12 AM.png・多线程处理所有请求,n个请求::n个线程,来一个请求new一个线程,创建新线程的操作是很昂贵的,JVM要为这个操作分配资 源。每个线程要在栈里保存自己的信息,64位的JVM里默认的栈空间是1024K,当因请求量增加导致new出相对应数量的线程数时, JVM就需要维护相对应数量的栈空间,如果是1000个请求,JVM光栈空间就需要1024K *1 000 =1G的RAM,每个线程自己在堆上产 生许多对象,会导致JVM的堆被占满,会导致JVM平凡GC,只要一GC,整个JVM就会stop-the-world,只要stop-the-word整个 JVM只会根据GC算法执行不同的GC,暂停处理我们的应用程序,影响应用性能的同时,也可能会导致JVM outofmemory、 stackoverilow。线程消耗不止RAM,还可能会消耗其它有限的资源,比如file descriptor文件描述符、socketfd socket描述符、文件 句柄、数据库连接。(linux的select单个进程所打开的FD是有一定限制的,默认值1024。 epoll并没有这个限制,它支持的FD上限是 操作系统的最大文件句柄数,这个数远大于1024。 1GB内存的机器大约是10万个句柄左右,通常这个值跟操作系统的内存关系较 大)过多消耗这些可能导致错误或系统崩溃。
Screen Shot 2020-06-17 at 11.13.18 AM.png. 线程池处理所有请求,m个请求: n个线程,通过线程池避免创建新线程,限制最大线程数量。线程池跟踪着所有线程,在线程数量达到上限前,它会创建新的线程 ,当有空闲线程时,它会使用空闲线程。
使用 jdk concurrent threadpool分为非固定的线程池(指定线程池中最小线程数和最大线程数的 ThreadPoolExecutors、 newCacheThreadPool)和固定大小(newFixedThreadPool.. newSingleTh read Exector)。 newCache丁h read Poo}这种线程还是有无法限制线程数量的问题,但是它会优先使用线程池中已创建的空闲线程来处理请求。这种类 型的线程池特别适用于执行短期任务的请求,因为它们不会长时间的阻塞外部资源。对于线程数量固定的线程池,当线程池中的线程 都在工作时,一个新的请求到达,丁h read Pool Executor可能会使用一个队列来组织新到达的请求,直到线程池中有空闲的线程可以使 用。Executors. nexFixedThread Pool方法默认创建一个没有长度限制的LinkedList (lnteger.MAX_VALUE)。这个LinkedList也可能会 产生系统资源耗尽的问题。队列中请求所占用的资源可能是file descriptor文件描述符、sockeffd socket描述符,而操作系统对同时打 开的FD是有限制的。因此,限制工作队列的长度也是有意义的。自定义Thread PoolExecutor,如果所有的线程都在执行任务,而且 工作队列也被请求填满了,此时对于新到达请求的处理方式,取决于Thread Pool Executor构造方法的最后一个参数。(Discard Policy 让线程池丢弃新到达的请求;AbortPol icy会让Executor抛出一个异常丢弃新到达的任务;CallerRunsPolicy会使任务在它的调用端线 程池中执行,可能会阻塞一个原来不应阻塞的线程;)
线程池中应该创建多少线程应该根据各个应用系统的经过负载测试后的数据来定。线程池中最大线程数应该被限制,这样才不会导致 系统资源被耗尽(系统资源包括内存堆栈,打开的file descriptor文件句柄、socketfd socket tcp连接、数据库连接等。如果线程执行 的是CPU密集型任务,计算型任务,创建的线程不应该超过CPU的内核数量)。
最佳实践
设置deadline,处理某个请求超过了timeout时间,这个线程就应该被停止,为新的请求腾出资源
failfast机制,如果线程执行没响应了,线程池中的线程数会迅速达到上限,这些线程都在等待前面没有响应的线程。使用failfast,所 有后续的请求都会迅速失败,而不是进行不必要的等待。
reactor模式,参考大神Doug Lea的 <<Scalable IO in Java>>
其它的并发处理方式
guava ListenableFuture
fork/join,
actor模式,akka并发就是基于actor模式
reactive programming,响应式编程,基于stream来处理,调用线程拿不到子线程的结果和异常
协程框架
网友评论