需要知道的几个概念
同步(Synchronous)和异步(Asynchronous)
同步方法调用一旦开始,调用者必须等到方法调用返回后,才继续执行后续行为。
异步方法一旦开始,方法调用会立即返回,调用者就可以继续执行后续行为。而异步方法通常会在另外一个线程“真实”地执行,当这个异步调用真实完成时,则会通知调用者。
并发(Concurrency)和并行(Parallelism)
并发偏重于多个任务交替执行,而多个任务之间有可能还是串行的。并行是真实意义上的“同时执行”。
临界区
临界区用来表示一种公共资源或者说是共享数据,可以被多个线程使用。但是每一次,只能有一个线程使用它,一旦临界区资源被占用,其他线程要想使用这个资源,就必须等待。
阻塞(Blocking)和非阻塞(Non-Blocking)
阻塞和非阻塞通常用来形容多线程间的相互影响。比如一个线程占用了临界区资源,那么其他需要这个资源的线程就必须在这个临界区中等待。等待会导致线程挂起,这种情况就是阻塞。
非阻塞的意思与之相反,它强调没有一个线程可以妨碍其他线程执行。所有线程都会尝试不断前向执行。
死锁(Deadlock)、饥饿(Starvation)和活锁(Livelock)
死锁、饥饿和活锁都属于多线程的活跃性问题。
死锁:A、B、C、D四辆小车在这种情况下都无法继续进行,它们彼此之间都占用了其他车辆的车道,如果大家都不愿意释放自己的车道,那么这个状态将永远维持下去,谁都不可能通过。
饥饿:是指某一个或者多个线程因为种种原因无法获得所需要的资源,导致一直无法执行。
活锁:如果两个或两个以上的线程都秉承着“谦让”的原则,主动将资源释放给他人使用,那么就会出现资源不断在两个线程中跳动,而没有一个线程可以同时拿到所有资源而正常执行。这种情况就是活锁。
并发级别
根据控制并发的策略,我们可以把并发的级别进行分类,大致分为阻塞、无饥饿、无障碍、无锁、无等待几种。
阻塞(Blocking)
一个线程是阻塞的,那么在其他线程释放资源之前,当前线程无法继续执行。
当我们使用synchronized
或重入锁时,我们得到的就是阻塞的线程。
无论是synchronized
或是重入锁,都会试图在执行后续代码后,得到临界区的锁,如果得不到,线程就会被挂起。
无饥饿
如果线程间有优先级,那么线程调度优先满足高优先级的线程。对于非公平的锁来说,系统允许高优先级的线程插队。这样有可能导致低优先级产生饥饿。
但是如果锁时公平的,满足先来后到,那么饥饿就不会发生。所有线程都有机会执行。
无障碍
无障碍是一种最弱的非阻塞调度。两个线程如果是无障碍的执行,那么他们不会因为临界区的问题导致一方被挂起。换言之,大家都可以进入临界区。
若大家一起修改共享数据,数据改坏了怎么办呢?对于无障碍的线程来说,一旦检测到这种情况,它就会立即对自己所做的修改进行回滚,确保数据安全。
悲观策略与乐观策略:如果说阻塞的控制方式是悲观策略(认为两个线程发生冲突的可能性很大),那非阻塞的调度就是一种乐观策略(认为两个线程发生冲突的可能性很小,用,如遇冲突,用回滚的方式解决)。
这个策略可能出现在临界区发生冲突时,所有线程可能都会不断回滚自己的操作,而没有一个线程走出临界区。
实现无障碍可以用一个“一致性标记”来实现。
无锁(Lock-Free)
无锁的并行都是无障碍的。在无锁的情况下,所有线程都能尝试对临界区进行访问,但不同的是,无锁的并发保证必然有一个线程能够在有限步内完成操作离开临界区。
在无锁的调用中,一个典型特点是可能会包含一个无穷循环。线程会不断尝试修改共享变量,无锁的并行总是能保证一个线程胜出。至于临界区失败的线程,它们会不断重试,直到胜出。如果总是不成功,则可能出现类似饥饿的现象。
无等待
无锁只要求有一个线程可以在有限步内完成操作,而无等待则在无锁的基础上更进一步进行扩展。它要求所有线程都必须在有限步内完成,这样就不会引起饥饿问题。
有关并行的两个重要定律
为什么要使用并行程序?有两个目的。第一, 为了获得更好的性能;第二,由于业务模型的需要,确实需要多个实体。我们关注更多的是性能的问题。
Amdahl定律
加速比定义:加速比 = 优化前系统耗时 / 优化后系统耗时
加速比越高,说明优化效果越好。
回到java:JMM
JMM的关键技术点都是围绕着多线程的原子性、可见性和有序性来建立的。因此,我们首先必须了解这些概念。
原子性(Atomicity)
原子性是指一个操作是不可中断的。即使是多个线程一起执行,一个操作一旦开始,就不会被其他线程干扰。
int
是原子性的、long
在32位系统来说,不是原子性的。
可见性(Visibility)
可见性是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。这只能发生在并行中。
有序性(Ordering)
在并发时,程序的执行可能会出现乱序。给人直观的感觉就是:写在前面的代码,会在后面执行。有序性问题的原因是因为程序在执行时,可能进行指令重排,重排后的指令与原指令的顺序未必一致。
指令重排可以保证串行语义一致,但是没有义务保证多线程间的语义也一致。
为什么要指令重排呢?完全是为性能考虑的。具体的原因可以翻原书。
哪些指令不能重排:Happen-Before规则
- 程序顺序原则:一个线程内保证语义的串行性
- volatile规则:volatile变量的写,先发生于读,这保证了volatile变量的可见性
- 锁规则:解锁必然发生在随后的加锁前
- 传递性:A先于B,B先于C,那么A必然先于C
- 线程的start()方法先于它的每一个动作
- 线程的所有操作先于它的每一个动作
- 线程的所有操作先于线程的终结(Thread.join)
- 线程的中断(interrupt())先于被中断线程的代码
- 对象的构造函数执行、结束先于finalize()方法