并发与并行
并发指的是同时应对多个事件的能力,并行指的是同时做多件事的能力。
- 位级并行:32位计算机能够同时处理32位数运算,而8位计算机却要进行多次运算。
- 指令级并行:表面上看cpu是串型执行的,单内部使用了流水线,乱序执行和猜测执行。
- 数据级并行:可以并行的在大量数据上施加同类操作,图像处理是一种非常适合数据级并行的场景。
计算机多处理器架构
SMP 对称多核架构:也叫统一内存访问架构,主要特征是所有cpu平等的共享所有资源,包括内存,io,总线等。
cpu公用一个总线访问内存,每个cpu有自己的缓存,缓存相互独立,会由于缓存产生一致性问题,解决这种问题有很多协议,常见的是MESI协议,其定义了一些缓存读写操作需要遵循的规范。
状态如下:
- Modified:本cpu写,则直接写到cache,不产生总线事件,其他cpu写,不涉及本cpu的cache,其他cpu读,本cpu需要把cache line的数据提供给他,而不让他去读内存。
- Exclusive:只有本cpu有该内存的cache,而且和内存一致。本cpu的写操作会导致转到modified状态。
- Invalid:一旦cache line进入这个状态,cpu读取数据必须发出总线事件,从内存读。通过扩展cpu数量可以提高这种架构性能,但是SMP服务器cpu利用率最好是2-4个cpu。
NUMA 非一致性内存访问架构:相对于SMP来讲,他是由多个cpu组成,每个cpu都有自己独立内存,总线,io等。不同cpu之间可以通过互联模块进行通信交互,使得每个cpu都可以访问整个服务所有内存,当访问本地内存的效率远高于远程内存。
MPP 大规模并行处理架构:也可以叫做分布式内存架构,cpu单元是地理上隔离的,交互通过网络进行,每个节点只能访问自己本地资源,是完全不共享的结构。但是其扩展性最好,类似于大型分布式集群系统。
java内存模型
java内存类似于SMP,但是其屏蔽了底层硬件环境的差异,给java提供了统一的内存访问模型。
java中所有线程共享主内存,对于每个线程都有自己的工作区,包括寄存器,栈,写换冲区,缓存,硬件,编译优化等。工作内存和主内存通过规定的操作进行数据同步,线程只能访问自己的工作内存,在多线程环境下,存在数据不一致问题。可以理解为:主内存是堆,工作内存是栈。
重排序
程序执行过程中,为提高性能,编译器和处理器通常对指令进行重排序。
- 编译器优化的重排序:在不改变但线程语义的情况下,可以重新安排语句的执行顺序。
- 指令级并行的重排序:如果不存在数据依赖,处理器可以改变语句对应及其指令的执行顺序。
- 内存重排序:由于处理器使用缓存,读写缓冲区,使得加载和存储操作看上去可以乱序执行。
指令和内存重排都属于处理器重排序。源代码 - 编译器重排 - 指令重排 - 内存重排 - 最终执行指令顺序。
为保证程序正确性,重排的原则有:
- 如果数据存在依赖情况,编译器不会改变存在数据依赖的操作顺序,但是不同线程之间和不同处理器之间的数据依赖不被编译器和处理器考虑。
- af-if-serial语义:不管怎么重排,单线程执行结果不能改变,编译器的runtime和处理器需要遵守此语义。
多线程并发
并发问题也就带来来线程安全访问的问题。多线程执行时需要考虑进行额外的协调。
不可变性:可变数据是引起不安全的主要原因,如果一个数据不可变,则不会存在数据安全问题。final,线程私有变量。
happens-befor:为解决编译器,处理器的重排问题,java引入了happen-befor原则,通过此概念可以定义操作之间内存可见性定义。
如果一个操作执行结果对另一个操作可见,那么这两个操作之间必须存在happen-befor原则。
- 程序次序原则:一个线程内如果编码a操作写在b操作前,则happen-befor。
- 监视器锁原则:对一个监视器解锁一定发生在后续对同一个监视器加锁之前,同时锁是同一把。
- volatile变量:写volatile变量一定发生在后续对他读之前。
- 线程启动原则:thread.start一定发生在线程中其他动作之前。
- 线程终止原则:线程中任何动作一定在动作完成之前执行。
- 线程中断原则:一个线程调用另一个线程的interrupt一定发生在另一个线程发现中断之前,通过thread.inerrupted()方法检测到是否有中断发生。
- 对象终结原则:一个对象的构造函数结束一定发生在对象finalize方法之前。
- 传递性原则:a发生在b之前,b发生在c之前,则a一定发生在c之前。
原子性
对象操作要么成功要么失败,不存在中间状态。
基本类型操作:
- int,char 数值读写,线程安全。
- long,double 读写分为高低位两部分,非线程安全。
- i++ 等组合操作,非线程安全。
对于不具备原子性的操作可以用sync或volatile关键字使其具有原子性,也可以使用原子类型的包装类。
可见性
当一个对象在多线程工作内存中有副本时,如果一个内存修改了共享变量,其他线程需要对其可见。
- final:初始化的final字段具有可见性,由于其不可变性。
- volatile:在语义上保证了新值能够立即同步到主内存中,每次使用时,需要从主内存拉取,保证了多线程的操作变量可见性。
- sync:在同步块内读写确保了可见性。
- happens-before:解决了大部分的可见性问题。
有序性
有序性保证多个线程对同一个对象有序的进行操作。