【并发编程笔记】 ---- CPU底层及缓存行知识

文章目录

1. 计算机的组成
2. 存储器的层次结构
3. Cache Line(缓存行)
4. 指令重排序

1. 计算机的组成

在这里插入图片描述
(自己理解的想法)简单叙述一下qq程序执行过程:
1. 运行前
qq.exe一开始在硬盘上,是一堆二进制代码,没有任何作用(程序只有进入了内存才能运行,进入内存后,需要服从操作系统的调度),在程序运行前,操作系统会进行程序的装载,也就是创建一个进程结构
2. 运行
CPU从程序中取出指令(在内存中),CPU读取一条指令到PC中(PC存储着下一条指令的地址),然后就会去内存中找到对应的数据存储到寄存器中,然后ALU对数据进行数据,再写回到内存中,这样的操作反复操作
(只是粗略的讲述一下简单过程,具体如何操作,需要去回顾一下汇编(我还没学))

2. 存储器的层次结构

在这里插入图片描述
在这里插入图片描述
多核CPU
在这里插入图片描述

  1. 什么是线程上下文切换?
    答:利用时间片轮转的方式,CPU给每个任务都服务一定的时间,然后把当前任务的状态保存下来,在加载下一任务的状态后,继续服务下一任务。任务的状态保存及再加载,这段过程叫做上下文切换。时间片轮转的方式使多个任务在同一颗CPU上执行变成了可能
    在这里插入图片描述

线程是CPU执行的基本单位,进程是CPU分配资源的基本单位

超线程: 一个ALU对应多个PC|Registers 所谓的四核八线程
四核八线程: 四核即代表着该CPU具有四个“CPU”,八线程则意味着每个CPU存有两个逻辑线程,总共有八个线程

3. Cache Line(缓存行)

Cache Line 是 CPU 和主存之间数据传输的最小单位。
Cache Line 常见大小为64字节,会在传输数据的时候,从内存中连续取64字节的数据。

在这里插入图片描述
伪共享
当CPU访问某个变量时,首先会去看CPU Cache内是否有该变量,如果有则直接从中获取,否则就去主内存里面获取该变量,然后把变量所在内存区域的一个Cache行大小的内存复制到Cache中。由于存放到Cache行的内存块而不是单个变量,所以可能会把多个变量存放到一个Cache行中。当多个线程同时修改一个缓存行里面的多个变量时,由于同时只能有一个线程操作缓存行,所以相比将每个变量放到一个缓存行,性能会下降,这就是伪共享

缓存行代码演示
演示1,两个线程的x会在同一个缓存行中

public class T01_CacheLinePadding {
	private static class T {
		public volatile long x = 0L;
	}

	public static T[] arr = new T[2];

	// 两个数组紧挨着的
	static {
		arr[0] = new T();
		arr[1] = new T();
	}

	public static void main(String[] args) throws InterruptedException {
		Thread t1 = new Thread(() -> {
			for (int i = 0; i < 1000_0000L; i++) {
				arr[0].x = i;
			}
		});

		Thread t2 = new Thread(() -> {
			for (int i = 0; i < 1000_0000L; i++) {
				arr[1].x = i;
			}
		});

		final long start = System.nanoTime();
		t1.start();
		t2.start();
		t1.join();
		t2.join();
		System.out.println((System.nanoTime() - start) / 100_0000);
	}
}
  • 变量不加volatile 结果平均耗时: 9毫秒
    由于Java中存在共享变量的内存不可见性问题,所以即使执行1000万次,每次都是从Cache中修改数据,两个线程不会互相通知已修改了数据
  • 变量加了volatile 结果平均耗时: 200多毫秒
    由于存在缓存行,arr[0]和arr[1]其实是紧挨着的,加入了volatile字段保证了内存可见性,一旦一个线程修改了数据,会通知另外一个已修改数据,会重新到主内存刷新最新的数据到Cache中

演示2: 两个线程的x不会在一个缓存行中

public class T02_CacheLinePadding {
	private static class Padding{
		public volatile long p1, p2, p3, p4, p5, p6, p7;
	}

	// Padding有56个字节数据 T有8个字节数据, 所以两个线程的X不会在一个缓存行中,即使加了volatile,效率也会大大提高
	// 两个线程的x不会相互影响
	private static class T extends Padding{
		public volatile long x = 0L;
	}

	public static T[] arr = new T[2];

	static {
		arr[0] = new T();
		arr[1] = new T();
	}

	public static void main(String[] args) throws InterruptedException {

		System.out.println(ClassLayout.parseInstance(arr[0]).toPrintable());
		
		Thread t1 = new Thread(() -> {
			for (int i = 0; i < 1000_0000L; i++) {
				arr[0].x = i;
			}
		});

		Thread t2 = new Thread(() -> {
			for (int i = 0; i < 1000_0000L; i++) {
				arr[1].x = i;
			}
		});

		final long start = System.nanoTime();
		t1.start();
		t2.start();
		t1.join();
		t2.join();
		System.out.println((System.nanoTime() - start) / 100_0000);
	}
}

测试结果耗时: 80多毫秒
原因: 因为一个缓存行大小为64字节,这里两个线程的x不可能在一个缓存行中,所以两个线程的x不相互影响

MESI Cache 一致性协议

  • Modified: 被修改
  • Exclusive: 独占
  • Shared: 共享
  • Invalid: 失效

CPU中每个缓存行(cache line) 使用4种状态进行标记:

  1. M: Modified(被修改过)
    该缓存行有效, 但是数据有脏状态(和内存中的数据不一致, 数据只存在于本Cache中)
    缓存行必须时刻监听所有试图读该缓存行相对旧主存的操作, 该操作必须在缓存中将缓存行写回主存中并将状态改为共享的(Shared)状态之前被延迟执行
  2. E: Exclusive(被排除的, 独占的)
    该缓存行有效, 数据和内存中的数据一致, 数据只存在于本缓存中
    缓存行也必须监听其他缓存读主存中该缓存行的操作, 一旦有该操作, 该缓存行需要变成共享转态
  3. S: Shared(共享的)
    该缓存行有效, 数据和内存中的数据一致, 数据存在于很多缓存中
    缓存行也必须监听其他缓存使该缓存行无效或者独占该缓存行的请求, 并将该缓存行变成无效的(Invalid)。
  4. I: Invalid(无效的)
    此缓存行无效(未被使用)

4. 指令重排序

Java内存模型允许编译器和处理器对指令重排序以提高允许性能,并且只会对不存在数据依赖性的指令重排序。在多线程下会存在问题。

如何避免指令重排序?
加入volatile关键字

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值