前言
程序的并发现象在我们日常编程中是常见的,但对并发的背后却很少深究。于是,笔者通过实践和阅读《Java并发编程艺术》、《实战Java高并发程序设计》、《七周七并发》等书籍来弥补自身并发这片空白,争取做到知其然知其所以然。
笔者计划将自己的学习笔记记录下来,整体规划是,并发中常用的名词概念(先得知道定义——what)->Java并发编程的基础(怎么使用Java并发编程——how)->并发机制底层实现原理(why)。
你应知道的
1. 并发和并行
并发:在一个时间片段内,多个任务被处理。注重点在能处理多个任务。
并行:在一个时间刻,多个任务被处理。注重点在同一时刻处理多个任务。
解释(盗用网上的图片):
图中上部分表示的是并发,两个串行任务队列使用的是一个处理机,它体现的是该处理机能处理多个串行队列,即拥有处理多个串行任务的能力。它好比我们的计算机的CPU,一个CPU有处理多个不同程序的能力。
图中下部分表示的并行,多个串行任务队列被多个处理机处理,它体现的是能在同一时刻处理多个串行任务队列,即同时处理多个串行任务的能力。它好比我们的多核计算机的CPU,多个CPU能同时处理多个程序的能力。
2. 并发中的重要定义
2.1 同步&异步
这两个概念主要是用来形容方法调用。(方法的调用也可以理解为消息的通知机制,方法的调用即消息的发送,方法的返回即消息的回应)。重点在程序的执行顺序。
同步 指的是方法调用后,调用者必须等到方法调用返回,才继续后续的行为,可以理解为程序的顺序执行,下一步执行的开始需要等待上一步执行的完毕。(其中调用者可以理解为该线程A程序)
异步 指的是方法调用后,调用者不需要等到方法调用返回,可以继续后续操作。而该调用的方法一般是会在另外一个线程B中真实执行,如果调用者需要该方法的返回结果,那么当线程B执行完该方法后会通知调用者(线程A)并将方法运行结果也一并返回。(其中调用者可以理解为该线程A程序)
2.2 阻塞&非阻塞
这两个概念主要是形容程序(该线程)等待消息回应时的状态。重点在程序(该线程)的状态。
阻塞 是指程序的调用结果返回之前,当前线程的状态是挂起状态,等待着方法的调用结果返回,不执行其他操作。这里阻塞调用和同步调用不同的是,同步调用等待方法的调用结果返回时,当前线程状态还是为激活状态,还可以接收处理其他消息,而阻塞调用中当前线程挂起不在处理任何其他业务。
非阻塞 非阻塞和阻塞的概念相对应,指在不能立刻得到方法返回结果之前,该函数不会阻塞当前线程,而会立刻返回。
2.3 临界区
临界区常见的定义,它表示一种共享数据或者公共资源,能被多个操作者访问,但当已经有一个操场者正访问或占用共享数据时,其他操作者则不能访问,如果想要访问则需等待,直到共享数据没有操作者访问。
2.4 死锁&饥饿&活锁
这三个概念描述的是多线程的活跃性问题。当发生这3个问题时,多线程也就会不在正常的活跃的工作。
死锁 指的是两个线程同时在请求对方占有的资源的情况。
饥饿 指的是一个线程在无限地等待其他线程占有的但是不会往外释放的资源的情况。
活锁 指的是任务或者执行者没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败——即线程对任务的处理没有取得任何进展的情况。 活锁和死锁的区别在于,处于活锁的实体是在不断的改变状态,所谓的“活”, 而处于死锁的实体表现为等待;活锁有可能自行解开,死锁则不能。活锁可以认为是一种特殊的饥饿。
2.5 并发的级别
由于临界区的存在,多线程之间的并发必须受到控制。根据控制并发的策略,可以把并发的级别进行分类,可以分为阻塞、无饥饿、无障碍、无锁、无等待。
阻塞 对临界区加锁来控制多线程的并发访问,得到临界区的锁便可以对临界区进行访问,否则线程将会被挂起等待(阻塞状态),直到获取了锁。通常使用synchronized或重入锁对临界区加锁。
无饥饿 对临界区的锁分为公平锁和非公平锁,一般来说,线程之间是有优先级的,所以线程调度的时候会倾向于调用优先级高的线程。非公平锁允许高优先级的线程优先获得临界区的锁。这种情况容易让低优先级的线程产生饥饿。公平锁则遵循先来后到原则,需要排队获取临界区的锁。
无障碍 多个线程对临界区的并发访问,不会因为一个线程占用了临界区而其他线程被挂起,多个线程可以都进入临界区,一旦发现修改共享数据的冲突,该线程便回滚该操作。是一种乐观的并发级别,相信多线程操作临界区的冲突概率不大。它的实现可以依赖一个“一致性标记”。线程操作临界区之前,读取并保存该标记,操作完后再次读取,检测是否更改。如果不一致,则重试操作。对于需要修改操作的线程,需要在修改数据前,更新这个一致性标记。
无锁 无锁(对临界区不加锁)的并行是无障碍的。所有的线程都能尝试对临界区进行访问,但不同的是,无锁的并发保证必然有一个线程能够在有限步内完成操作离开临界区,一个典型的特点是可能会包含一个无穷循环。在这个循环中,线程会不断尝试修改共享变量。如果修改成功,程序退出,否则继续尝试修改。其中CAS算法便是无锁的并发级别。
无等待 无等待在无锁的基础上更进一步,要求所有的线程都必须在有限步内完成对临界区的操作。一种典型的无等待结构就是RCU(Read-Copy-Update)。它的基本思想是,对数据的读可以不加控制。因此,所有的读线程都是无等待的,它们既不会被锁定等待也不会引起任何冲突。但在写数据的时候,先取得原始数据的副本,接着只修改副本数据(这就是为什么读可以不加控制),修改完成后,在合适的时机回写数据。
3. 并发编程考虑的问题
在了解了并发中相关的定义后,需要进一步考虑并发编程中的下面几个问题。
3.1 上下文切换
理解:我们知道,线程的可运行状态到运行状态的变化条件是CPU给该线程分配了CPU时间片和运行资源。单核多线程的运转是通过单个CPU不停的切换时间片和切分资源给线程来达到的多线程的执行。时间片的切换伴随着线程间的切换,每个时间片下不一定能完全运行完该线程,于是每次切换的时候都需要保存线程的任务状态,不停的加载线程的状态,这时候便会给系统带来附加的消耗。
思考:在解决多任务问题时,并不是越多线程处理效果越好,需要将线程的上下文切换的消耗给考虑进去,避免不必要的线程的创建和使用。可以考虑使用无锁并发策略、CAS算法等。
3.2 死锁
思考:对临界区进行加锁来控制并发的访问,这是一个非常实用的方法,非常利于理解。但是当我们使用锁来进行并发编程的时候,需要时常考虑好是否会有死锁的情况发生。
3.3 资源限制的问题
理解:并发编程的时候,程序执行的速度常受限于计算机的硬件资源和软件资源。常考虑的硬件资源限制有带宽的上行、下行速度,硬盘读写速度和CPU处理速度。软件资源限制有数据库的连接数和socket连接数等。例如,服务器下载的速度2M/s,线程下载资源的速度是1M/s,如果创建10个线程来下载的话,下载速度并不会提升到10M/s,因为受限于资源。
思考:因为资源的限制,有时候串行的代码执行速度会快于并行代码的执行速度,因为并行执行的时间还需额外加上的上下文切换和资源调度的时间。这时候,可以考虑集群的方式来增添系统的资源,或者适当的调整并发度。