多线程基础知识
同步Sync和异步ASync
同步和异步用来形容一次方法调用。
- 同步方法调用一旦开始,调用方必须等待方法返回才能继续后续行为。
- 异步方法调用更像是一个消息传递,一旦开始,方法调用就会立即返回,调用者可以继续后续操作。
临界区
临界区是一种公共资源,或者称为共享数据,可以被多个线程使用。但是对于每一个临界区资源,同一时刻只能有一个线程来使用它,一旦该临界区资源被占用,其它线程想要使用该资源,就必须等待。
临界区资源不一定限制数量为1,例如n台打印机就允许n个线程在并行使用。
阻塞blocking和非阻塞non-blocking
阻塞和非阻塞通常用来形容多线程间的相互影响。
比如一个线程占用了临界区资源,那么其它所有需要这个临界区资源的线程就必须在这个临界区等待。等待导致线程挂起,这种情况就是阻塞。
非阻塞强调没有一个线程可以妨碍其它线程执行,所有线程都尝试不断向前执行。
死锁、饥饿、活锁
活锁:线程都秉承“谦让”的原则,主动将资源释放给他人使用,造成没有一个人真正获取了执行所需的全部资源。生活中的例子:两人让路,同时向左,同时向右
并发级别
由于临界区的存在,必须对多线程进行并发控制。策略有:
阻塞blocking
使用synchronized关键字、可重入锁时,得到的就是阻塞的线程。
当前线程在无法得到临界区的锁时,就会挂起等待,进入阻塞状态。阻塞的线程在其它线程释放资源之前,当前线程无法继续执行。
无饥饿starvation-free
如果线程之间是有优先级的,那么这种资源的调度就是不公平的。对于非公平的锁,系统允许高优先级的线程插队,这样有可能造成低优先级的线程饥饿。对于公平的锁,先到先服务,就不会产生饥饿现象。
无障碍obstruction-free
无障碍是一种最弱的非阻塞调度。如果两个线程是无障碍的执行,那么他们不会因为临界区的问题导致另一方挂起。
所有的线程都可以进入临界区修改共享数据,如果检测到冲突,就会对自己修改的数据进行回滚。
阻塞的方式是一种悲观策略,认为两个线程之间很有可能发生不幸的冲突,因此以保护共享数据为第一优先级。
无障碍的方式是一种乐观策略,认为系统中发生冲突的可能性或概率不大,真发生了冲突再进行回滚。这种策略不适合存在严重冲突的系统。
无障碍方式可以通过一个“一致性标记”来实现。
无锁lock-free
所有线程都能够尝试对临界区进行访问,但最终只能有一个线程胜出。其它的线程迟早能够得到执行权,因此系统不会出现无限等待。无锁方式(CAS)是对无障碍方式的一种改进,无障碍方式中系统可能出现无限等待。
疑问:
无障碍方式是先修改后判断标识,无锁方式是先判断标识后修改吗?
CAS方式只能做一些简单的修改吗?例如,线程A首先获取到修改数据的权利,想要把name和age都修改。但是线程B只需要修改name,
无等待wait-free
无锁只要求一个线程可以在有限步内完成操作,而无等待则在其基础上更进一步,要求所有线程都在有限步内完成。如果限制这个步骤上限,还可以进一步细分为有界无等待和线程数无关的无等待,它们的区别只是对循环次数的限制不同。
典型的无等待结构时RCU(Read-Copy-Update),基本思想是:对读操作不加控制,在写数据的时候,先取得原始数据的副本,接着只修改副本数据,修改完成后,在合适的时机回写数据。
疑问:什么是合适的时机呢?
Amdahl定律
加速比 = 优化前耗时 / 优化后耗时
在假设有无穷多的CPU内核情况下,加速比和程序的并行化比例成反比,即如果有50%的代码支持并行,那么系统加速比理论上界是2。这个理论指出仅仅提高CPU数量不能够根本上提高系统的性能。
JMM(Java内存模型)
JMM 的关键技术点都是围绕多线程的原子性、可见性和有序性来建立的
原子性Atomicity
对于32位的JVM系统来说,long型数据的读写不是原子性的,因为long型数据有64位。也就是说如果两个线程对long型数据进行写入的话,对线程之间的结果是有干扰的。例如:
A线程写入long型数据:XY
B线程写入long型数据:ZW
假设A线程先写入X,B线程再写入ZW,然后A线程再写入Y,最终保存下来的long型数据就会是ZY。同理,也可能产生XW。
// 测试代码, 多个线程写入固定的数字, 一个线程去读取, 如果不是这些固定的数字, 就输出
可见性Visibility
可见性是一个综合问题,是由于编译器优化(指令重排)、硬件优化、缓存优化等行为产生的。
以缓存优化为例,CPU1修改和保存的是缓存中的数据,CPU2无法意识到这个行为。
包括指令重排在内的编译器优化更是产生一些在并发分析的理论上看似不可能出现的情况,因为实际执行的代码顺序和所编写的代码顺序可能不一致,理论分析是针对所编写的代码顺序而言的。简单来说,编译器优化会以串行程序优化的思路修改你所编写的串行或并行代码,因此针对你所编写的代码进行并发分析可能的输出结果和实际的输出结果可能不相同。
有序性Ordering
指令重排:保证串行语义的一致性
指令重排造成各种并发问题,那为什么还有指令重排呢?性能!
指令重排是为了尽量少的中断CPU流水线。CPU流水线的执行流程:
- 取指令IF
- 译码和取寄存器操作数ID
- 执行或者有效地址计算EX
- 存储器访问MEM
- 写回WB
如果禁止指令重排,那么如果指令和指令之间必须存在停顿(插入一个null),相当于其后所有的指令都浪费了一个单位的时间,(指令数量多造成浪费大,你浪费1s,就是浪费全班1min)。因此,与其浪费这一个单位时间,不如指令重排序,在不影响结果(串行程序)的前提下把这一个单位的时间给利用上。