操作系统底层原理与JMM模型
一、操作系统底层原理
想玩转并发编程,前提一定是对计算机底层模型有一定的了解。线程、进程、CPU、内存模型之间到底是以怎样的关系,来和谐的完成用户每次触发的任务,JVM模型在其中又是以怎样的作用呢?
1.1 冯诺依曼模型
上图是针对现代计算机抽象出来的简化理论模型,由图可见计算机的理论模型由计算器(CPU)、存储器(内存)等设备组成。
如果对比现代计算机的硬件结构可以展现为下图。
1.2 操作系统缓存与内存管理
1.2.1 操作系统缓存
上述操作系统中最为核心的就是CPU、内存。
CPU实际是通过指令进行操作的,各CPU品牌通过预存指令集,来与其他设备交互。而CPU的更新迭代则是围绕如何让“CPU闲下来”开展的(指令缓存L1 —> 数据缓存L1—> 流水线 —> 超线程 —> 多核处理器 —> 多核超线程)。
现代CPU(多核处理器)为了提升执行效率,一般在CPU上集成多级缓存架构,来减少CPU与内存的交互。常见的三级缓存结构:
- L1 Cache,数据缓存和指令缓存(逻辑核独占)
- L2 Cache,物理核独占,逻辑核共享
- L3 Cache,所有物理核共享
CPU高效缓存:为了解决CPU高速数据访问的问题,在CPU访问存储设备时,无论是存储数据或存储指令,都趋于聚集在一片连续的区域中,这被称为局部性原理。
- 时间局部性(Temporal Locality):如果一个信息项正在被访问,那么在近期它很可能被再次访问。比如循环、递归、方法的反复调用等。
- 空间局部性(spatial Locality):如果一个存储器的位置被引用,那么在它附近的位置也会被引用。比如顺序执行的代码、连续创建的多个对象、数组等。
举例空间局部性原则的案例:
public class JmmOS_01_Math {
private static final int RUNS =100;
private static final int short_edge = 6;
private static final int long_edge = 1024*1024;
private static long[][] longs;
public static void main(String[] args) {
/**
* 初始化数组
*/
longs = new long[short_edge][];
for (int i = 0; i < short_edge; i++) {
longs[i] = new long[long_edge];
for (int j = 0; j < long_edge; j++) {
longs[i][j] = 1L;
}
}
System.out.println("Array初始化完毕....");
Long start_1 = System.currentTimeMillis();
Long total2 =0L;
for (int k = 0; k < RUNS; k++) {
for (int i=0;i<long_edge;i++){
for (int j = 0; j < short_edge; j++) {
total2+=longs[j][i];
}
}
}
System.out.println(total2);
System.out.println(System.currentTimeMillis() - start_1);
//CPU有三级缓存,读取数据时有空间局部性,每次数据附近的值也会被读取
Long start = System.currentTimeMillis();
Long total =0L;
for (int k = 0; k <RUNS ; k++) {
for (int i=0;i<short_edge;i++){
for (int j = 0; j < long_edge; j++) {
total+=longs[i][j];
}
}
}
System.out.println(total);
System.out.println(System.currentTimeMillis() - start);
}
}
1.2.2 操作系统内存管理
操作系统的内存空间,出于程序运行安全隔离与稳定的考虑,是由用户空间和内核空间隔离开的。下图以32位操作系统的4GB内存来举例。
- 用户空间
从0x00000000 到 0xc0000000(PAGE_OFFSET) 的线性地址可由用户代码 和 内核代码进行引用。 - 内核空间
从0xc0000000(PAGE_OFFSET)到 0xFFFFFFFFF的线性地址只 能由内核代码进行访问。
由于内存空间地址的隔离划分,CPU调度的线程也被划分为:内核线程模型(KTL)和用户线程模型(UTL)。
1.3 线程与进程
- 进程
操作系统正在运行的一个程序,会创建一个进程。进程是OS(操作系统)资源分配的最小单位。 - 线程
线程是OS(操作系统)调度CPU的最小单元,也叫轻量级进程(Light Weight Process)。一个进程里可以创建多个线程,这些线程各自拥有计数器、堆栈、局部变量等属性。CPU在线程上高速切换,给使用者感觉这些线程在同时执行的感觉,即并发的概念。线程的上下文切换过程:
二、JMM模型
JMM描述的是一组规则,通过这组规则控制程序中各个变量在共享数据区域和私有数据区域的访问方式。JMM是围绕原子性、有序性、可见性展开的。
2.1 JMM模型
下图是基于JMM规范的线程、工作内存和主内存的交互图。其中:
- 主内存:主要存储Java实例对象,所有线程穿件的实例对象都存放在出内存中,不管实例对象时成员变量还是方法中的本地变量,也包括共享的类信息、常量、静态变量。(这些数据有可能存放在堆、栈、方法区等)
- 工作内存:主要存储当前方法的所有本地变量信息(从主内存中拷贝的变量副本),每个线程只能访问自己的工作内存,即线程中的本地变量对其他线程是不可见的,就算多线程执行相同的代码、相同的变量,它们自己也会在各自的工作内存中创建当前线程的本地变量。工作内存是每个线程的私有数据,线程间无法相互访问。
- JVM规范:如果实例对象的成员方法中,其本地变量是基本类型(boolean、byte、short、char、int、long、float、double),将直接存储在工作内存的栈帧结构中,如果本地变量是引用类型,那么该变量的引用会存储在栈帧中(存储地址)。
- 实例对象的成员变量,不管是基本类型还是引用类型,都会被存储在堆中。至于static变量及类本身相关信息会被存储在主内存中。
- 在主内存中的实例对象会被多线程共享,倘若多个线程同时调用一个对象的同一个方法,那么私有线程将会将需要操作的数据拷贝到自己的工作内存副本,执行完成后才刷新到主内存。
2.2 JMM存在的必要性
假如有2个线程Thread-a、Thread-b两个线程同时对主内存中的initFlag的值进行操作,a线程是读取操作,b线程是将initFlag的值由false置为true。倘若b,a线程分别启动,a线程读取到initFlag的值是什么呢?这就需要有一致性的协议约定主内存与工作内存的交互。
2.3 数据同步八大原子操作
- 1)lock锁定:作用域:主内存的变量,功能:把一个变量标记为线程独占
- 2)unlock解锁:作用域:主内存变量,功能:把一个处于锁定状态的变量释放,释放后可以被其他线程锁定
- 3)read读取:作用域:主内存的变量,功能:拷贝副本。把主内存中的变量拷贝到工作内存
- 4)load载入:作用域:线程工作内存变量,功能:read操作读取到的变量放入到共享变量副本中
- 5)use使用:作用域:工作内存的变量,功能:把工作内存中的变量传递给执行引擎
- 6)assign赋值:作用域:工作内存的变量,功能:从执行引擎接受到的值,赋给工作内存变量
- 7)store存储:作用域:工作内存的变量,功能:把工作内存中的变量值传送到主内存
- 8)write写入:作用域:主内存的变量,把传递到主内存的值写入到主内存的变量中
如2.2节汇总所示,如果需要将一个变量从主内存汇总复制到工作内存中,就需要顺序执行read和load操作。如果需要将变量从工作内存同步到主内存中,就需要顺序执行store和write操作。
2.4 并发编程三大特性
2.4.1 原子性
概念:原子性指的是一个操作不可中断,即使多线程环境下,一个操作一旦开始就不会被其他线程影响。
示例:基本数据类型中byte、short、int、float、boolean、char的读写是原子性的,但由于long、double长度为64位,那么他们的操作并不是原子的。
2.4.2 可见性
可见性指的是一个线程修改了主内存共享变量的值后,其他线程能否感知到主内存中该值发生了变化,并对本地工作变量副本中该值进行更新。
示例:A线程修改了共享变量的值,但尚未写回主内存,此时线程B读取到了共享变量的值,并拷贝到本地副本,线程A更新后的值是否能让线程B的本地副本感知?
2.4.3 有序性
单线程中,代码总是按照顺序依次执行,但在多线程环境下,有可能出现乱序问题,因为程序编译成机器码指令后可能出现指令重排现象。(下面代码,来解释实际指令重拍的现象,for循环中,将a,b,x,y变量初始值均设置为0,但这4个变量没有特殊的修饰关键字。每次循环中,开启2个新线程,并对a,b完成赋值为1,主线程等待2个新线程结束后,查看x,y的值。如果按照代码顺序只会有1-0,0-1,1-1三种结果,但如果指令发生重拍会出现第四种结果0-0)
public class JmmOS_05_CodeReOrder {
private static int x= 0,y = 0;
private static int a= 0,b = 0;
public static void main(String[] args) throws InterruptedException {
int i=0;
for (;;){
i++;
x=0;y=0;
a=0;b=0;
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
shortWait(10000);
a=1;
x=b;
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
shortWait(10000);
b=1;
y=a;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
String result = "第" + i + "次(" + x + "," + y + ")";
if (x ==0 && y == 0){
System.out.println(result);
break;
}else {
System.out.println(result);
}
}
}
/**
* 等待一段时间,纳秒
*/
public static void shortWait(long interval){
long start = System.nanoTime();
long end;
do {
end = System.nanoTime();
} while (start + interval >=end);
}
}