并发编程核心概念与主要内容
# 并发编程核心概念与主要内容
# 核心概念
# 进程、线程、管程
- 进程:指在系统中能独立运行并作为资源分配的基本单位,它是由一组机器指令、数据和堆栈等组成的,是一个能独立运行的活动实体。
- 线程:线程是进程的基本执行单元,一个进程的所有任务都在线程中执行;进程要想执行任务,必须得有线程
- 管程:Monitor(监视器),也就是我们平时所说的锁,在操作系统上实际上定义了一个数据结构和在该数据结构上的能为并发进程所执行的一组操作,这组操作能同步进程和改变管程中的数据。
- 一种同步机制。保证了同一时刻只有一个进程在管程内活动,即管程内定义的操作在同一时刻只被一个进程调用(由编译器实现)。但是,这样并不能保证进程以设计的顺序执行。
- JVM中同步时基于进入和退出的管程对象(Monitor),每个对象实例都有一个Monitor对象,随着 java 对象一同创建和销毁。执行线程首先要持有管程对象,然后才能执行方法,当方法完成之后会释放管程,方法在执行时候会持有管程,其他线程无法再获取同一个管程。
进程与线程的区别:同一进程的线程共享本进程的地址空间,而进程之间则是独立的地址空间;同一进程内的线程共享本进程的资源,而进程间的资源是独立的。
线程与管程的关系:通常,执行线程要先持有管程,然后才能执行方法,最后当方法执行完成(无论成功还是失败)释放管程。在方法执行期间,其他任何线程无法再获取同一个管程。
# 并发与并行
串行:串行表示所有任务都按先后顺序执行。当有多个任务要执行,每次只能执行一个任务,执行完后继续下一个任务,所有任务顺序执行。
并行:并行表示系统可以同时取得多个任务,并同时去执行所取得的这些任务。并行模式相当于将长长的一条队列,划分成了多条短队列,所以并行缩短了任务队列的长度。简单来说就是系统中同一时刻运行了多个任务,这些任务可能最后会汇总。例如:泡方便面,电水壶烧水,一边撕调料倒入桶中
并行的效率从代码层次上强依赖于多进程/多线程代码,从硬件角度上则依赖于多核 CPU。
另一个定义:同时在某个数据集的不同部分上运行同一任务的不同实例就是并行。
并发(concurrent):指在单个处理器上采用单核执行多个任务。在这种情况下,操作系统的任务调度程序会很快从一个任务切换到另一个任务,因此看起来所有的任务都是同时运行的。简单来说就是系统中同时运行了多个任务,也可以理解为同一时刻多个线程在访问同一个资源。例如,春运抢票 电商秒杀...。
并发的重点在于它是一种现象,描述的是多进程同时运行的现象。对于单核心 CPU 来说,同一时刻只能运行一个线程。所以,并发的"同时运行"表示的不是真的同一时刻有多个线程运行的现象,而是提供一种功能让用户看来多个程序同时运行起来了,但实际上这些程序中的进程不是一直霸占 CPU 的,而是执行一会停一会(时间片)。
在程序员看来,并行可以看作将任务和它们对共享资源的访问同步的不同技术和机制的方法。要解决大并发问题,通常是将大任务分解成多个小任务, 由于操作系统对进程的调度是随机的,所以切分成多个小任务后,可能会从任一小任务处执行。这可能会出现一些现象:1. 可能出现一个小任务执行了多次,还没开始下个任务的情况。这时一般会采用队列或类似的数据结构来存放各个小任务的成果。2. 可能出现还没准备好第一步就执行第二步的可能。这时,一般采用多路复用或异步的方式,比如只有准备好产生了事件通知才执行某个任务。3. 可以多进程/多线程的方式并行执行这些小任务。也可以单进程/单线程执行这些小任务,这时很可能要配合多路复用才能达到较高的效率。
并发与并行的概念十分相似,而且这种相似性随着多核处理器的发展也在不断增强。
# 同步
在并发中,我们可以将同步定义为一种协调两个或更多任务以获得预期结果的机制。同步的方式有两种:
- 控制同步:例如,当一个任务的开始依赖于另一个任务的结束时,第二个任务不能再第一个任务完成之前开始。
- 数据访问同步:当两个或更多任务访问共享变量时,再任意时间里,只有一个任务可以访问该变量。
与同步密切相关的一个概念时临界段。临界段是一段可以访问共享资源的代码,在任何给定时间内,只能被一个任务执行。互斥是用来保证这一要求的机制,当然也可以采用不同的方式来实现。
同步可以帮助你在完成并发任务的同时避免一些错误,但是它也为你的算法引入了一些开销。你必须非常仔细地计算任务的数量,这些任务可以独立执行,而无需进行算法中的互通信。这就涉及并发算法的粒度。如果算法有着粗粒度(低互通信的大型任务),同步方面的开销就会较低。然而,也许你不会用到系统所有的核心。如果算法有着细粒度(高互通信的小型任务),同步方面的开销就会很高,而且该算法的吞吐量可能不会很好。
并发系统中有不同的同步机制。从理论角度看,最流行的机制如下:
- 信号量(semaphore):一种用于控制对一个或多个单位资源进行访问的机制。它有一个用于存放可用资源数量的变量,而且可以采用两种原子操作来管理该变量。
- 互斥(mutex,mutual exclusion的简写形式)是一种特殊类型的信号量,它只能取两个值(即资源空闲和资源忙),而且只有将互斥设置为忙的那个进程才可以释放它。互斥可以通过保护临界段来帮助你避免出现竞争条件。
- 监视器:一种在共享资源上实现互斥的机制。它有一个互斥、一个条件变量、两种操作(等待条件和通报条件)。一旦你通报了该条件,在等待它的任务中只有一个会继续执行。
如果共享数据的所有用户都受到同步机制的保护,那么代码(或方法、对象)就是线程安全的。例如,数据非阻塞的CAS(compare-and-swap,比较和交换)原语是不可变的,这样就可以在并发应用程序中使用该代码而不会出任何问题。
# 不可变对象
不可变对象是一种非常特殊的对象。在其初始化后,不能修改其可视状态(其属性值)。如果想修改一个不可变对象,那么你就必须创建一个新的对象。
不可变对象的主要优点在于它是线程安全的。你可以在并发应用程序中使用它而不会出现任何问题。不可变对象的一个例子就是java中的String类。当你给一个String对象赋新值时,会创建一个新的String对象。
# 原子操作和原子变量
与应用程序的其他任务相比,原子操作是一种发生在瞬间的操作。在并发应用程序中,可以通过一个临界段来实现原子操作,以便对整个操作采用同步机制。
原子变量是一种通过原子操作来设置和获取其值的变量。可以使用某种同步机制来实现一个原子变量,或者也可以使用CAS以无锁方式来实现一个原子变量,而这种方式并不需要任何同步机制。
# 共享内存与消息传递
任务可以通过两种不同的方式来相互通信。
- 共享内存:通常用于在同一台计算机上运行多任务的情况。任务在读取和写入值的时候使用相同的内存区域。为了避免出现问题,对该共享内存的访问必须在一个由同步机制保护的临界段内完成。
- 消息传递:通常用于在不同计算机上运行多任务的情形。当一个任务需要与另一个任务通信时,它会发送一个遵循预定义协议的消息。如果发送方保持阻塞并等待响应,那么该通信就是同步的;如果发送方在发送消息后继续执行自己的流程,那么该通信就是异步的。
# 用户线程和守护线程
- 用户线程:平时用到的普通线程、自定义线程
- 守护线程:运行在后台的特殊线程,比如垃圾回收线程
当主线程结束后,如果用户线程还在运行,守护线程存活,如果没有用户线程,都是守护线程,那么守护线程停止。
# 主要内容
# 并发编程三要素
- 原子性:即一个不可再被分割的颗粒。在Java中原子性指的是一个或多个操作要么全部执行成要么全部执行失败。
- 有序性:程序执行的顺序按照代码的先后顺序执行(处理器可能会对指令进行重排序)。
- 可见性:当多个线程访问同一个变量时,如果其中一个线程对其作了修改,其他线程能立即获取到最新的值。
# 线程的五大状态
- 创建状态:当用 new 操作符创建一个线程的时候
- 就绪状态:调用 start 方法,处于就绪状态的线程并不一定马上就会执行 run 方法,还需要等待CPU的调度
- 运行状态:CPU 开始调度线程,并开始执行 run 方法
- 阻塞状态:线程的执行过程中由于一些原因进入阻塞状态比如:调用 sleep 方法、尝试去得到一个锁等等
- 死亡状态:run 方法执行完 或者 执行过程中遇到了一个异常
# 悲观锁与乐观锁
- 悲观锁:每次操作都会加锁,会造成线程阻塞。
- 乐观锁:每次操作不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止,不会造成线程阻塞。
# 线程间协作
线程间的协作有:wait/notify/notifyAll等
# synchronized
synchronized是Java中的关键字,是一种同步锁。它修饰的对象有以下几种:
- 修饰一个代码块:被修饰的代码块称为同步语句块,其作用的范围是大括号{}括起来的代码,作用的对象是调用这个代码块的对象
- 修饰一个方法:被修饰的方法称为同步方法,其作用的范围是整个方法,作用的对象是调用这个方法的对象
- 修饰一个静态的方法:其作用的范围是整个静态方法,作用的对象是这个类的所有对象
- 修饰一个类:其作用的范围是synchronized后面括号括起来的部分,作用主的对象是这个类的所有对象。
# CAS
CAS全称是Compare And Swap,即比较并替换,是实现并发应用到的一种技术。操作包含三个操作数—内存位置(V)、预期原值(A)和新值(B)。 如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。
CAS存在三大问题:ABA问题,循环时间长开销大,以及只能保证一个共享变量的原子操作。
# 线程池
如果我们使用线程的时候就去创建一个线程,虽然简单,但是存在很大的问题。如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。
线程池通过复用可以大大减少线程频繁创建与销毁带来的性能上的损耗。