转载

学习Java并发(1)基本概念

在工作中时常接触到并发环境,只是现有的框架已经在底层封装好了,可以直接调用。但总会有一些奇特的场景需要自己手动实现并发,所以了解原理是很重要的。本文用于记录学习并发过程中重要的点或思考。

本文只讨论基本概念,旨在用较为详细易懂的文字记录自己对于并发的一些理解。

什么是并发

并发(Concurrency)是指系统在同一时间段可同时处理多个任务,而同一时刻只有一个任务处于运行状态。

实际上对于单核CPU而言无法实现真正的同时运行多个任务。但是由于CPU运算速度极快,在一个短的时间单位内(比如0.001秒)执行一个任务,执行完马上切换到下一个任务。这样宏观看起来就像是在同时运行多个任务了。

与**并行(Parallel)**的区别:并行是指同一时刻可以同时运行多个任务。现代多核处理器都支持并行运算。并发强调系统支持多个任务同一时刻存在;并行强调系统支持多个任务同时运行。

CPU核心数

可以理解为多处理器,双核CPU从程序的角度来看就等于可共享资源的两个单核处理器。对于单核处理器而言,任一时刻只能处理一个任务;然而多核处理器可以在同一时刻同时处理多个任务。由于现在的CPU基本都支持所谓的 超线程 ,使得一个物理核心可以逻辑上实现两个任务的同时运行(比如Intel的i5处理器通常都是4核8线程,6核12线程),所以程序角度来看一个核心,系统在同一时刻可以同时运行的任务数为:CPU超线程数 * CPU个数。

线程与进程

基本概念

进程(Process)是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础,同时也是线程的容器。

线程(Thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。一条线程指的是进程中一个单一顺序的控制流,一个进程中可以并发多个线程,每条线程并行执行不同的任务。

一个操作系统拥有多个进程,一个进程拥有至少一个线程。一个程序拥有一个或多个进程,通常应用为一个进程,但诸如Chrome、Firefox等应用也会拥有多个进程。一般而言进程之间无法共享资源,因为进程拥有独立的内存空间、资源分配等,多个进程之间互不影响;但线程却天然共享内存空间,所以多线程应用最大的问题就在于如何规避资源被多个线程同时访问,而多进程应用最大的问题就在于进程间通信。

线程的调度

CPU调度的基本单位是线程。对于进程的调度来说不同的系统之间存在不同的方式,这里不做探讨。

对于单核CPU而言,系统会针对每一个可执行线程分配所谓的 时间片 (指当前线程可执行时间),当该线程执行超过执行时间或离开可运行态时,CPU将会切换到下一个线程,如果之前线程未执行完,则暂停执行,等待下一次时间片分片;线程之间有优先级的划分,高优先级线程将有更多的几率获取到时间片。对于多数服务器而言,其使用的至强系列服务级芯片通常拥有6核以上,再加上超线程技术的加持,其可同时处理至少12个线程,线程获取到时间片的频次也大大增加。CPU拥有很高的主频,也就是其执行指令计算的时钟频率,主频越高,频率越快,一个时间单位能够执行的指令条数也就更多,一个线程所耗费的时间片也就会变少,所有线程的调度也会更加频繁。

对于一台服务器而言,程序的线程数并不是越大越好。线程数过大(上千上万)显然是不合适的,创建线程会消耗很多的系统资源,CPU切换上下文所耗费的成本也会更多,大量的线程也会长时间处于等待状态而无法及时处理。合适的数量大致如下:

线程数 = CPU核心数 / (1 - 阻塞系数) 阻塞系数 = 阻塞时间 / (阻塞时间 + 计算时间)

假设每个线程其阻塞时间为8个时间单位,计算时间为2个时间单位(对于大多数I/O系统而言,其阻塞时间通常大于计算时间,也就是说线程访问I/O多数时间处于等待状态),那么其阻塞系数就为0.8;那对于一个双核处理器而言,最好为其开启十个线程,才能不浪费系统资源,同时也能够兼顾到所有的线程。(由于不了解操作系统,我也一直在思考阻塞时难道文件系统不使用CPU吗?暂时先这么理解吧)

扩展: 协程 (Coroutine)又名纤程,可以理解为线程中的线程。一个进程可有多个线程,同时一个线程也可拥有多个协程。在一些高并发的原生语言中(比如 golang )自然支持,同时Java中也有第三方的库( quasar )支持。线程的切换需要依靠CPU执行上下文切换会产生开销,且支持的最大线程数有限,但协程是在程序内部实现的切换,不存在切换开销,而且协程存在于一个线程当中,不需要锁,无需担心写变量冲突的问题,所以其执行效率远高于线程。单台服务器可能最多支持几百个线程,但支持上百万的协程,如此可以极大地提升系统地并发能力。

线程的状态

线程拥有五种基本状态:

  • 创建:一个线程刚刚被创建出来;
  • 就绪:线程目前处于可运行状态,等待获取时间片;
  • 运行:线程获取到时间片,正在运行;
  • 阻塞:线程因为一些原因放弃当前CPU,且该阶段无法获取CPU时间片;
  • 死亡:线程执行完毕或产生异常被中止。

说法比较简略,但大致差不多。网络上对于这些状态的讨论众说纷纭,这里只简单描述一下抽象的概念。

阻塞与非阻塞

首先是 阻塞 。线程的几种状态中,阻塞状态是程序通常需要面临的一个问题。由于阻塞会导致当前线程暂停执行,放弃CPU时间片,如果所有的线程都要面临长时间的阻塞,则系统的性能也会面临瓶颈。

当线程访问一些资源时(I/O资源),由于当前资源不可获取,则当前线程放弃CPU的使用权,等待资源可获取。等待资源的这个状态则被称之为阻塞。当线程为阻塞状态时,什么事也不做。资源一旦可用,则会触发一个事件,使当前线程进入可运行态,重新等待CPU的时间片分配。这只是宏观上的理解,对于底层而言,系统要做的事情非常复杂。同样当线程面临同步访问变量时,等待锁分配的过程也被称为阻塞状态。简单而言,线程等待共享资源的使用权这个阶段就被称为阻塞状态。

我们的很多程序都要面临阻塞的问题。比方文件I/O,网络I/O,或者是其他硬件资源例如打印机等。比如在访问文件资源时,java是通过文件流来进行访问的。 FileInputStream.read() 这个方法在源码中实际上是调用了一个由本地C代码实现的native方法: FileInputStream.read0() 。该方法从文件中读取一个字节到jvm内存当中,这个过程是阻塞的(在java的官方文档中可以查看,使用IDE开发时查看源码也能看到相应的注释)。从更底层的角度来理解阻塞的话,应该是java程序调用read()方法即向本地文件系统发出读取字节的请求,而这个字节是由文件系统来进行准备的。文件系统与JVM实际上是属于不同的程序,也就是不同的进程,文件系统在准备好这个字节之前JVM只能等待,所以CPU暂停当前的Java线程,使当前线程处于阻塞状态,也就是不可执行、不可获取时间片的一个状态,等待这个字节已准备好后,再将当前线程设置为可运行态,也就是就绪状态。当此线程再次获取到CPU时间片之时,这个程序也就得以继续运行下去。网络I/O也是如此,比如HTTP请求、JDBC数据库访问等。

通常对于服务提供程序运行来说,阻塞的时间消耗往往大于程序执行的时间消耗,这类应用也被称之为 I/O密集型应用 ,比如资讯类网站、查询系统等;然而另一类应用,比如数据分析、深度学习之类的应用,它们不会面对较多的I/O操作,更多的是数据计算,这类应用被称之为 计算密集型应用 。对于前者,设置足够多的线程已使得程序能够充分利用CPU资源;而对于后者,直接增强硬件的并行计算能力,然后设置合理的线程或进程数量(通常不要超过核心数)才能保证程序的效率,因为切换线程的操作对于CPU而言也是需要消耗资源的。

假如程序几乎不会产生阻塞的话,并发模型也就没有什么意义了,那么线程数只需要设置为CPU核心数就好了。如果是单核CPU,直接串行化执行即可,无需考虑并发问题。

阻塞与 非阻塞 所强调的都是线程的状态。非阻塞自然是指当前线程在临界情况时不会进入阻塞状态,程序能够直接往下执行。比如读取文件,假如 read() 方法是非阻塞的,那么在java发起请求后,直接返回read的结果,可能文件系统比较快,此时已经读取出了一个字节可供直接返回,又或者没能读取到而返回-1,程序都会接着执行下去。对于非阻塞的请求,通常需要在程序中做更多的工作来处理资源获取的完整性。有关非阻塞的讨论,之后继续进行。

同步与异步

同步与异步,从概念上来看,其关注的是消息的通信方式。同步强调发出一个调用或一个请求时,必须等待其返回结果,才能够执行下一步操作。而异步强调的是发出请求后直接进行下一步操作,不需要马上关心返回结果。对于程序员而言,这两个概念都是在抽象感知的层面解释的,而在代码层面,其实与多线程或多进程并无干系。比如一个资源服务器,调用接口后直接返回缓存好的图片、文本等,资源都是只读的(在网络图床、内容发布系统中应用广泛),通常服务都是并发提供的,即开启了多进程/多线程,然而服务本身却是同步的,因为调用了接口就要等待资源返回;而异步则通常依靠函数回调、事件轮询等机制实现。典型的比如NodeJS,其机制就是单线程异步模型,内部依靠事件轮询来实现异步。

事件轮询可以理解为,在主线程中执行完了相应的事件注册后,则开始执行一个死循环,也就是轮询,用来监听所有注册的事件是否被触发。触发的方式很简单,通过触发的线程来操作共享的标识资源,当轮询检测到标识为触发状态,则开始调用相应的事件函数。执行完毕后,继续循环。其实NodeJS本身实现也是依靠了多线程,只是提供给程序员感知的就只有一个单线程而已。其内核为Chrome浏览器的V8引擎,其实浏览器执行Javascript也是这样的,编写的代码执行起来一定是单线程,但浏览器内置了轮询线程、事件监听线程等等。事件轮询模型非常适合界面程序,使用较少的线程/进程资源且能够保持程序的高效运行。

也有其他很多的各种模型,比如同步阻塞(典型的Java的I/O流)、同步非阻塞、异步阻塞、异步非阻塞,其实现方式也多种多样,在此不再进行深入探讨。

最后

本文只是用笔者比较口语化的文字简单解释了并发编程中常见的一些基本概念,解释的不是很完整,毕竟真要彻底解释其中所有从大到小从底层到抽象的所有概念的话得写本很厚的书才行,我只希望这篇文章可以为接下来要记录的Java并发编程相关笔记提供一个比较能理解的理论总结。可能很多的理解并不全面(也许查阅的资料不太齐全),其中可能有错,也欢迎各位指正。

原文  https://juejin.im/post/5e10bfcce51d4541625a4fdb
正文到此结束
Loading...