并行基础概念
1.同步与异步
- 同步synchronous:方法一旦开始调用,调用者必须等到方法返回后,才能继续后续的行为。比如先做饭、后炒菜。
- 异步asynchronous:方法开始调用后,调用者可以进行后续操作,等待方法返回结果使用。比如做饭时进行炒菜动作。
2.并发与并行
2者都可以标识多个任务一起执行。
- 并发偏重于多个任务交替执行,但有可能是串行的。
- 并行即同时开始执行。
3.临界区
表示一种公共资源或者共享数据,可以被多个线程来使用,但是每一次只能有一个线程使用。一旦被占用,下一个使用者必须等待占用结束。比如一台打印机,在打印的A同时,后面的B必须等待前面的A结束。
4.阻塞和非阻塞
用来形容多线程间的相互影响。当一个线程占用了临界区的资源,那么所有需要该资源的线程都必须在这个临界区中等待。这样导致线程挂起,进入阻塞状态。非阻塞强调没有线程妨碍其他线程执行。
5.死锁、饥饿、活锁
- 死锁,即线程之间互相把持着对方需要的资源,无法进行后续执行陷入死锁状态。
- 饥饿,即某些线程一直无法获取继续执行下去的资源,导致程序无法执行。在线程优先级太低、某些线程一直占据资源的情况下可能出现,这就需要特别的控制算法。
- 活锁,当线程之间保持谦让原则,则可能互相释放自己的资源给对方使用,使得资源来回跳转没有任一线程获取足够资源,导致都无法继续执行下去。
6.并发级别
- 阻塞
阻塞线程在无法获取足够资源之前无法执行。使用synchronized、重入锁在等待临界区资源时该线程就为阻塞挂起,直到获取足够资源为止。 - 无饥饿
线程之间存在优先级,则资源会优先满足给优先级高的线程,那么优先级低的线程容易进入饥饿。如果锁是公平的,满足先来后到,那么就不会出现饥饿。 - 无障碍
是一种最弱的非阻塞调度,线程之间不会因临界区资源而被挂起。线程会一起进入临界区,当存在数据竞争时,无障碍线程会回滚自己的操作,否则继续执行直到结束。阻塞控制方式可以称为悲观策略,一旦线程之间发生冲突,便以保护共享数据为第一优先级。非阻塞可称为乐观策略,认为线程之间不会或者概率很小发生冲突;线程进行无障碍执行,但一旦发现冲突便应该回滚。可使用“一致性标记”来实现无障碍执行,操作前获取、保存该标记,操作结束后再次获取并检查是否一致,一致则无冲突,否则反之需要回滚重新进行。 - 无锁
无锁的并行是无障碍的,这时所有线程均能访问临界区,但是需保证必然有一个线程能够在有限步内完成操作离开临界区。 - 无等待
要求所有线程均能在有限步内完成,而不引起饥饿问题。控制步骤上限也可分为有界无等待、线程数无关的无等待。RCU(Read-Copy-Update)便是无等待的:对数据的读不进行控制,但在写数据时先获取原始数据副本,只修改副本数据,之后在合适的时机回写数据。
7.原子性
指一个操作是不可中断的,即便多个线程一起执行,一个操作一旦开始就不会被其他线程干扰。比如一个static int变量,多个线程同时进行赋值,则赋值唯一不可中断。但是long类型在32位系统下不是原子性的,因为long有64位。
8.可见性
指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。缓存优化、硬件优化、指令重排、编辑器优化等都有可能导致可见性问题。
9.有序性
在同一个线程内,代码是依次执行的。但在并发时,程序的执行可能会出现乱序,进行指令重排。指令重排保证串行语义一致,但在多线程间的语义一致没有保证。
以下原则是指令重排无法违背的:
- 程序顺序原则:一个线程内保证语义的串行性
- volatile规则:volatile变量的写,先发生于读,保证了volatile变量的可见性
- 锁规则:解锁必然发生在随后的加锁前,如果对一个锁解锁后,再加锁,那么加锁的动作绝对不能重排到解锁动作之前。否则无法获得该锁。
- 传递性:A先于B,B先于C,那么A必然先于C
- 线程的star()方法先于它的每一个动作
- 线程的所有操作先于线程的终结(Thread.join())
- 线程的中断(interrupt)先于被中断线程的代码
- 对象的构造函数执行、结束先于finalize()方法