前言
并发处理的广泛应用使得Amdahl定律代替摩尔定律成为计算机性能发展源动力的根本原因,也是人类压榨计算机运算能力的最有力武器。注:Amdahl定律通过系统中并行化与串行化的比重来描述多处理器系统能够获得的运算加速能力,摩尔定律则用与描述处理器晶体管数量与运行效率之间的发展关系。这两个定律的更替代表了近年来硬件发展从追求处理器频率到追求多核心并行处理的发展过程。
导图概述
Java内存模型
一、背景
1.I/O操作大多数的运算任务,处理器至少要与内存交互,如读取运算数据、存储运算结果等I/O操作。由于计算机的存储设备与处理器的运算速度有几个数量级的这种硬件效率差距,所以现代计算机需要加入接近处理器运算速度的高速缓存Cache来作为内存与处理器之间的缓冲。
2.缓存一致性
在多处理器系统中,每个处理器都有自己的高速缓存,而它们共享同一个主内存Main Memory,为保证同步回主内存时数据一致性,各个处理器访问缓存时需要遵循一些协议,如MSI、MESI、MOSI等。
3.内存模型
内存模型可以理解为在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象,如图所示,它们之间的关系。
不同架构的物理机器可以拥有不一样的内存模型,而Java虚拟机也有自己的内存模型,并且文中介绍的内存访问与硬件的缓存访问操作具有很高的可比性。
二、Java内存模型
1.产生Java虚拟机规范中定义一种Java内存模型(Java Memory Model JMM)来屏蔽各种硬件和操作系统的内存访问差异,以实现Java程序在各种平台下都能达到一致的内存访问效果。
2.主要目标
定义程序中各个变量的访问规则,在虚拟机中将变量存储到内存和从内存中取出变量的底层细节。变量包括实例字段、静态字段、数组对象的元素,不包括局部变量与方法参数,因为后者是线程私有的,不会被共享。
3.三部分
主内存存储所有的变量,主内存仅是虚拟机内存的一部分,不同于物理硬件的主内存;工作内存保存了该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作必须在工作内存中进行,不能直接读写主内存的变量。线程间的变量值传递需要通过主内存完成。
线程、主内存、工作内存三者的交互关系如下图所示:
从变量、主内存、工作内存的定义看,主内存主要对应于Java堆中的对象实例数据部分,工作内存对应于虚拟机栈中的部分区域。从更低层次上说,主内存直接对应于物理硬件的内存,为了获取更好的运行速度,虚拟机可能会让工作内存优先存储于寄存器和高速缓存中,因为程序运行时主要访问读写的是工作内存。
内存间交互操作
一、原子性特征:8种操作
1.作用于主内存的变量操作lock(锁定)、unlock(解锁)、read(读取)、write(写入)
2.作用于工作内存的变量操作
load(载入)、use(使用)、assign(赋值)、store(存储)
二、有序性、可见性特征--volatile型变量、先行发生原则
1.地位:是Java虚拟机提供的最轻量级的同步机制2.两种特性
(1)可见性:指一条线程修改了这个变量的值,新值对于其他线程来说是立即得知的。volatile变量也可以存在不一致的情况,由于每次使用之前都要先刷新,执行引擎看不到不一致的情况,因此可以认为不存在一致性问题。
Java运算并非原子操作,导致volatile在并发下是不安全的,如下所示代码:
public class VolatileTest {
public static volatile int race=0;
public static void increase() {
race++;
}
private static final int THREADS_COUNT=20;
public static void main(String[] args) {
Thread[] threads=new Thread[THREADS_COUNT];
for (int i = 0; i <threads.length ; i++) {
threads[i]=new Thread(new Runnable() {
@Override
public void run() {
for (int j = 0; j <10000 ; j++) {
increase();
System.out.println(race+" 一个线程中的值,i="+j);
}
}
});
threads[i].start();
}
System.out.println(race);
}
}
这段代码发起了20个线程,每个线程都对race变量进行10000次自增操作,正确结果应该是200000,但是运行后发现,结果都是小于200000的数字。客观的说,即使编译出来只有一条字节码指令,解释器将要运行许多行代码才能实现它的语义;如果编译执行,一条字节码指令也可能转化成若干条本地机器码指令。这说明执行这条指令不是一个原子操作。
使用volatile关键字需要满足两条规则,否则需要synchronized或java.until.concurrent中原子类保证原子性。
- 运算结果不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
- 变量不需要与其他的状态变量共同参与不变约束。
(2)禁止指令重排序:volatile屏蔽指令重排序的语义在JDK1.5中才被完全修复。关键的变化在于,volatile修饰的变量,赋值后多执行了一个相当于内存屏障的操作。重排序时不能把后面的指令重排序到内存屏障之前的位置。
因此,volatile变量读操作的性能消耗和普通变量没有差别,但是写操作会慢一些,因为它需要在本地代码中插入许多内存屏障指令保证处理器不发生乱序执行。
3.long和double型变量的特殊规则
64位的数据类型(long、double),允许虚拟机在没有volatile修改的情况下读写操作划分为两次32位的操作进行。可以不保证load、store、read和write的原子性。
三、java与线程
1.线程线程是比进程更轻量级的调度执行单位,可以把一个进程的资源分配和执行调度分开,各个线程可以共享进程资源(内存地址、文件I/O等),又可以独立调度。
2.线程实现-3种方式
内核线程:操作系统内核通过操作调度器对象成进行调度,并负责将线程的任务映射到各个处理器上。
每个内核线程相当于内核的分身,程序一般不会直接使用内核线程,通过内核线程的一种高级接口-轻量级进程(Light Weight Process,LWP )
用户线程:完全建立在用户空间的线程库上,系统内核不能感知线程存在。
用户线程加轻量级进程混合实现
3.Java线程调度
指为线程分配处理器使用权的过程,主要有两种协同式线程调度和抢占式线程调度。协同式线程调度,由线程本身控制执行时间;抢占式线程调度由系统分配执行时间。
4.5种状态
新建New、运行Runnable、等待(Waiting、Timed Waiting)、阻塞Blocked、结束Terminated