一直以来并发编程对于没接触过的人来说总是觉得高深莫测,当然也有像我这样的,虽然接触过,但是知识点还是太过片面,不够系统化。于是乎,我就诞生了写多线程入门到入土一系列文章的想法,以提升自己对并发编程的理解和认知。(小声BB:我不会说这是威哥逼迫我去写的!)

e7032c1d35063b91955f0d548cc2a780.gif

进程、线程与任务

进程

进程,大家估计是很了解的了,在Windows下打开任务管理器,可以发现我们在操作系统上运⾏的程序都是进程:

542b373aab7da0ade8495c0d19a8818a.png

进程(Process)是程序的运行实例。例如,一个运行的Typora就是一个进程。而运行一个 Java 程序的实质是启动一个 Java 虚拟机进程,也就是说一个运行的 Java 程序就是一个 Java 虚拟机进程。

进程的定义:

进程是程序的⼀次执⾏,进程是⼀个程序及其数据在处理器上顺序执⾏时所发⽣的活动,进程是具有独⽴功能的程序在⼀个数据集合上运⾏的过程,它是系统进⾏资源分配和调度的⼀个独⽴单位。

进程是系统进⾏资源分配和调度的独⽴单位。每⼀个进程都有它自己的内存空间和系统资源。

线程

那系统有了进程这么⼀个概念了,进程已经是可以进⾏资源分配和调度了,为什么还要线程呢 ???

fe0cd37e1ba31d7a26e5d18855184700.gif

为使程序能并发执⾏,系统必须进⾏以下的⼀系列操作:

  • 创建进程,系统在创建⼀个进程时,必须为它分配其所必需的、除处理器以外的所有资源,如内存空间、I/O 设备,以及建⽴相应的 PCB
  • 撤消进程,系统在撤消进程时,⼜必须先对其所占有的资源执⾏回收操作,然后再撤消 PCB
  • 进程切换,对进程进⾏上下⽂切换时,需要保留当前进程的 CPU 环境,设置新选中进程的 CPU 环境,因⽽须花费不少的处理器时间。

引⼊线程主要是为了提⾼系统的执⾏效率减少处理器的空转时间和调度切换的时间,以及便于系统管理,使 OS 具有更好的并发性。简单来说:进程实现多处理⾮常耗费 CPU 的资源,⽽我们引⼊线程是作为调度和分派的基本单位。

一个进程在其执行的过程中可以产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间作切换工作时,负担要比进程小得多。

任务

线程所要完成的计算就被称为任务,特定的线程总是在执行着特定的任务。任务代表线程所要完成的工作,它是一个相对的概念。一个任务可以是从服务器上下载一个文件、 解压缩一批文件、解压缩一个文件、监视某个文件的最后修改时间等。这些任务也正是相应线程存在的理由。

总结

  • 进程作为资源分配的基本单位。
  • 线程作为资源调度的基本单位,是程序的执⾏单元,执⾏路径(单线程:⼀条执⾏路径,多线程:多条执⾏路径)。是程序使⽤ CPU 的最基本单位。
  • 任务是指线程所要完成的计算内容。

多线程编程

什么是多线程编程

多线程编程就是以线程为基本抽象单位的一种编程范式。但是,多线程编程又不仅仅是使用多个线程进行编程那么简单,其自身又有其需要解决的问题

简单来说,多线程编程类似于“和尚挑水”的故事:一个和尚挑水喝,两个和尚担水喝,三个和尚没水喝。在这个故事中,一个和尚挑水会比较吃力,因此每天能运上山的水也非常有限。 两个和尚一起担水,每个人都省点儿力,因此他们每天运的水会比一个和尚挑的水要多一些。但是,三个和尚在一起的结果就是大家都不想去打水,最后导致没有水喝!如果把这个故事中的和尚比作线程而把打水比作这些线程所要完成的任务,那么我们不难发现增加线程可能会增加单位时间内完成的任务量,即提高程序的计算效率;但它也可能降低程序的计算效率(如故事中最后大家没有水喝)。可见,多线程编程并非使用多个线程进行编程那么简单。

为什么要使用多线程

  • 多核的CPU的背景下,催生了并发编程的趋势,通过并发编程的形式可以将多核CPU的计算能力发挥到极致,性能得到提升
  • 在特殊的业务场景下先天的就适合于并发编程。比如当我们在网上购物时,为了提升响应速度,需要拆分,减库存,生成订单等等这些操作,就可以进行拆分利用多线程的技术完成。面对复杂业务模型,并行程序会比串行程序更适应业务需求,而并发编程更能吻合这种业务拆分

总结:
1、充分利用多核CPU的计算能力
2、方便进行业务拆分,提升应用性能

优势和风险

多线程编程具有以下优势:

  • 提高系统的吞吐率。多线程编程使得一个进程中可以有多个并发的操作。例如,当一个线程因为I/O操作而处于等待时,其他线程仍然可以执行其操作。
  • 提高响应性。在使用多线程编程的情况下,对于GUI软件(如 桌面应用程序)而言,一个慢的操作(比如从服务器上下载一个大的文件)并不会导致软件的界面出现被“冻住”的现象而无法响应用户的其他操作;对于Web 应用程序而言,一个请求的处理慢了并不会影响其他请求的处理。
  • 充分利用多核处理器资源。如今多核处理器的设备越来越普及,就算是手机这样的消费类设备也普遍使用多核处理器。实施恰当的多线程编程有助于我们充分利用设备的多核处理器资源,从而避免了资源浪费
  • 最小化对系统资源的使用。一个进程中的多个线程可以共享其所在进程所申请的资源(如内存空间),因此使用多个线程相比于使用多个进程进行编程来说,节约了对系统资源的使用。
  • 简化程序的结构。线程可以简化复杂应用程序的结构。

多线程编程也有自身的问题与风险,包括以下几个方面:

  • 线程安全问题。多个线程共享数据的时候,如果没有采取相应的并发访问控制措施,那么就可能产生数据一致性问题,如读取脏数据(过期的数据)、丢失更新(某些线程所做的更新被其他线程所做的更新覆盖)等。
  • 线程活性问题。一个线程从其创建到运行结束的整个生命周期会经历若干状态。从单个线程的角度来看,RUNNABLE状态是我们所期望的状态。但实际上,代码编写不当可能导致某些线程一直处于等待其他线程释放锁的状态,即产生了死锁。另外,线程是一种稀缺的计算资源,一个系统所拥有的处理器数量相比于该系统中存在的线程数量而言总是少之又少的。某些情况下可能出现线程饥饿的问题,即某些线程永远无法获取处理器执行的机会而永远处于RUNNABLE状态的READY子状态。
  • 上下文切换。处理器从执行一个线程转向执行另外一个线程的时候操作系统所需要做的一个动作被称为上下文切换。由于处理器资源的稀缺性,因此上下文切换可以被看作多线程编程的必然副产物,它增加了系统的消耗,不利于系统的吞吐率。
  • 可靠性。从提高软件可靠性的角度来看,某些情况下可能要考虑多进程多线程的编程方式,而非简单的单进程多线程方式。

应该了解的概念

同步VS异步

同步异步通常用来形容一次方法调用。

同步方法调用一开始,调用者必须等待被调用的方法结束后,调用者后面的代码才能执行。

而异步调用,指的是,调用者不用管被调用方法是否完成,都会继续执行后面的代码,当被调用的方法完成后会通知调用者。

比如,在超时购物,如果一件物品没了,你得等仓库人员跟你调货,直到仓库人员跟你把货物送过来,你才能继续去收银台付款,这就类似同步调用。而异步调用了,就像网购,你在网上付款下单后,什么事就不用管了,该干嘛就干嘛去了,当货物到达后你收到通知去取就好。

串行、并发与并行

  • 串行
    • 多个任务,执行时一个执行完再执行另一个。
    • 例:吃完饭再看电视。
  • 并发
    • 并发性是指同⼀时间间隔内发⽣两个或多个事件。
    • 并发是在同⼀实体上的多个事件。
    • 例: 一会跑去厨房吃饭,一会跑去客厅看电视。
  • 并行
    • 并⾏性是指同⼀时刻内发⽣两个或多个事件。
    • 并⾏是在不同实体上的多个事件。
    • 例:一边吃饭一边看视频。

阻塞和非阻塞

阻塞非阻塞通常用来形容多线程间的相互影响,比如一个线程占有了临界区资源,那么其他线程需要这个资源就必须进行等待该资源的释放,会导致等待的线程挂起,这种情况就是阻塞,而非阻塞就恰好相反,它强调没有一个线程可以阻塞其他线程,所有的线程都会尝试地往前运行。

临界区

临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每个线程使用时,一旦临界区资源被一个线程占有,那么其他线程必须等待。