走入并行世界
在图像处理和服务器端程序中,是可以、且需要使用并行技术的。原因是,对于图像处理,它往往拥有极大的计算量;而服务器端程序,一方面,它需要承受很重的用户访问压力;另一方面,它往往拥有很复杂的业务模型。面对复杂的业务模型,并行程序会比串行程序更容易适应业务需求,更容易模拟现实世界。比如:JVM中,执行MAIN函数,做JIT编译,GC等任务是相对独立的。我们不应该将没有关联的业务代码拼凑在一起,分离为不同的线程更容易理解和维护。因此,使用并行也不完全出自性能的考虑,有时候,我们会很自然地那么做。
并行的几个基本概念
同步和异步:用来形容一次方法调用。
并发和并行:并发偏重于多个任务交替执行,而多个任务之间有可能还是串行的;并行是真正意义上的“同时执行”。如果系统中只有一个CPU,这种情况下的多进程或者多线程实际上是并发的,而非并行。真正的并行只可能出现在拥有多个CPU的系统中。
临界区:表示一种公共资源或者共享数据,可以被多个线程使用。但是每一次,只能有一个程序使用它,一旦临界区资源被占用,其他线程想要使用这个资源,就必须等待。
阻塞和非阻塞:用来形容多个线程间的相互影响。
死锁、饥饿和活锁:都属于多线程的活跃性问题。如果发现这几种情况,那么相关线程可能就不再活跃,也就是说它可能很难再继续往下执行了。
死锁最糟糕。
饥饿是指某一个或多个线程因为种种原因无法获取所需要的资源,导致一直无法执行。比如它的线程优先级太低,而高优先级的线程不断抢占它需要的资源;或,一个线程一直占着关键资源不妨,导致其他需要这个资源的线程无法正常执行。
活锁:资源不断在两个线程中跳动,而没有一个线程可以同时拿到所有资源而正常执行。
并发级别
由于临界区的存在,多线程之间的并发必须受到控制。根据控制并发的策略,可以把并发级别大致分为阻塞、无饥饿、无障碍、无锁、无等待几种。
阻塞:Synchronized, 重入锁
无饥饿:公平锁
无障碍:乐观策略。多个线程无障碍执行,但是一旦检测到冲突,就应该进行回滚。一种可行的实现可以依赖于一个“一致性标记“来实现。线程在操作之前,先读取并保存这个标记,在操作完成之后再次读取,检查这个标记是否呗更改过,如果两者是一致的,则说没资源访问没有冲突;否则,说明资源在操作过程中与其他写线程冲突,需要重试。
无锁:无锁的并行都是无障碍的。在无锁的情况下,所有的线程都尝试对临界区进行访问,但不同的是,无锁的并发保证必然有一个线程能够在有限步内完成操作离开临界区。一个典型的例子:
无锁无限循环无等待:要求所有的线程都必须在有限步内完成。一种典型的无等待结构是RCU(Read-Copy-Update),其基本思想是,对数据的读可以不加控制;但是在写数据的时候,先取得原始数据的副本,接着只对副本进行修改,修改完成后,在合适的时机回写数据。
并行的性能
串行系统并行化后的加速比定义:
加速比 = 优化前系统耗时 / 优化后系统耗时
加速比与系统的串行化率成反比。为了提高系统的速度,仅增加CPU处理器的数量并不一定能起到有效的作用。需要从根本上修改程序的串行行为,提高系统内可并行化的模块比重,在此基础上,合理增加并行处理器数量,才能以最小的投入,得到最大的加速比。
JMM
JMM的关键技术点都是围绕多线程的原子性、可见性和有序性来建立的。
原子性:一个操作是不可中断的。对于32位系统来说,LONG型数据的读写就不是原子的(因为LONG有64位)。如果两个线程同时对LONG进行写入的话(或者读取),对线程间的结果是有干扰的。
可见性:是一个综合性问题。缓存优化、硬件优化、指令重排及编译器的优化等都有可能导致一个线程的修改不会立即被其他线程察觉。
有序性:有序性问题的原因在于程序在执行时,可能会进行指令重排。一条指令的执行是可以分为很多步骤的,指令重排对于提高CPU的处理性能是十分有必要的。但指令重排可以保证串行语义一致,但是没有义务保证多线程间的语义也一致。
Happens-Before规则:
1. 程序顺序原则:一个线程内保证语义的串行性
2. volatile规则:volatile变量的写,先发生于读,这保证了volatile变量的可见性
3. 锁规则:解锁必然发生在随后的加锁前
4. 传递性:A先于B,B先于C,那么A必然先于C
5. 线程的start()方法先于它的每一个动作
6. 线程的所有操作先于线程的终结Thread.join()
7. 线程的中断interrupt()先于被中断线程的代码
8. 对象的构造函数执行、结束先于finalize()方法
以上规则都是为了保证指令重排不会破坏原有的语义结构。
引用:
《JAVA高并发程序设计》第一章:走入并行世界