CPU,CPU核心数,CPU线程数
中央处理器(CPU,central processing unit):作为计算机系统的运算和控制核心,是信息处理、程序运行的最终执行单元。
CPU核心:称为内核,是CPU最重要的组成部分。CPU内核是CPU的核心芯片。具有固定的逻辑结构,一级缓存、二级缓存、执行单元、指令级单元和总线接口等逻辑单元都会有科学的布局。
CPU核心数:也就是单块CPU上能处理数据的芯片组的数量。如双核,四核,八核等
CPU线程数:如果没有超线程技术,一个CPU核心对应一个线程
英特尔的超线程技术:模拟CPU核心数,是在CPU核心内部仅复制必要的资源、让两个线程可同时运行。只能提升性能40%左右(处理器内部缓存会被划分成几区域,互相共享资源)
一个CPU类比一个家庭,多核心是家庭里有多个成员(爸妈老公儿子),所有家庭成员都可以干活。而多线程技术,就比如一个人有2双手可以干活,但是性能肯定没有两个人1双手干活高效。
例如:i5有两种,早先i5也是有双核心四线程的(伪四核),也有四核心四线程的(真四核)。i7处理器是四核八线程(伪八核)
多核和多CPU区别:
简单说就是一家子人干活和几家人干活的区别,假设总人数一样,一家人开个门就能协调,几家人就要过街才能商量
多核CPU性能最好,但成本最高;多CPU成本小,便宜,但性能相对较差。
多线程和CPU

在执行任务时,CPU 会先将运算所需要使用到的数据复制到高速缓存中,让运算能够快速进行,当运算完成之后,再将缓存中的结果刷回(flush back)主内存,这样 CPU 就不用等待主内存的读写操作了。

线程是 CPU 调度的最小单位,线程中的字节码指令最终都是在 CPU 中执行的。
线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的工作内存(local memory),工作内存中存储了该线程以读 / 写共享变量的副本。
线程的工作内存:只是对 CPU 寄存器和高速缓存,以及其他的硬件和编译器 的优化的抽象描述。

高并发引发的问题
使用高速缓存提高读写速度,多线程提高CPU的吞吐量,优化语句或者指令执行次序提高性能但同时。也带来可见性、原子性、有序性问题,造成多线程程序结果不可控。
缓存导致的可见性问题
多核时代,每颗 CPU 都有自己的缓存,可能CPU 缓存与内存的数据一致性的问题。

在命令式编程中没,线程间的通信机制有两种:共享内存和消息传递。(线程间的消息传递通过主存)
如果主存共享变量count=0,线程A对共享变量count+=1,但是线程A没有及时把更新后的值刷入到主内存中,而此时线程B从主内存读取共享变量count=0 (原始值),那么我们就说对于线程B来讲,共享变量count的更改对线程B是不可见的。
这就是缓存的可见性问题。
可见性:多个线程共享一个变量,当一个线程改变了这个变量的值,其他线程能立即看到修改的值
线程切换带来的原子性问题

count += 1,至少需要三条 CPU 指令。
- 指令 1:首先,需要把变量 count 从内存加载到 CPU 的寄存器;
- 指令2:之后,在寄存器中执行 +1 操作;
- 指令 3:最后,将结果写入内存(缓存机制导致可能写入的是 CPU 缓存而不是内存)。
操作系统做任务切换,可以发生在任何一条 CPU 指令执行完,而不是高级语言里的一条语句。所以得到的结果可能不符合预期

原子性:一个操作要么都执行,要么都不执行。(一个或者多个操作在 CPU 执行的过程中不被中断的特性)
编译优化带来的有序性问题
在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。但是会保证程序最终结果会和代码顺序执行结果相同。
重排序分三种类型:
- 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
- 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
- 内存系统的重排序。由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
上述的 1 属于编译器重排序,2 和 3 属于处理器重排序。

例如编译器优化的重排:

原因在于,处理器在进行重排序时,会考虑到指令之间的依赖性。如果指令a=a+1必须用到指令a=1的结果,那么处理器会保证指令a=1在指令a=a+1之前执行。
又例如利用双重检查创建单例对象的案例
public class Singleton {
static Singleton instance;
static Singleton getInstance(){
if (instance == null) {
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton();
}
}
return instance;
}
}
我们以为的 new 操作应该是:
- 分配一块内存 M;
- 在内存 M 上初始化 Singleton 对象;
- 然后 M 的地址赋值给 instance 变量。
但是实际上优化后的执行路径却是这样的:
- 分配一块内存 M;
- 将 M 的地址赋值给 instance 变量;
- 最后在内存 M 上初始化 Singleton 对象。
双重检查创建单例的异常执行路径:

这是因为指令重排序了,可以在instance加上volatile,禁止重排序。
在多线程编程中,看起来没有问题,实际上会出现问题。只要我们能够深刻理解可见性、原子性、有序性在并发场景下的原理,很多并发 Bug 都是可以理解、可以诊断的。
导致可见性的原因是缓存,导致有序性的原因是编译优化。那解决可见性、有序性的办法是按需禁用缓存和编译优化。Java 内存模型
Java 内存模型(解决可见性、有序性问题)
内存模型是一套共享内存系统中多线程读写操作行为的规范,这套规范屏蔽了底层各种硬件和操作系统的内存访问差异,解决了 CPU 多级缓存、CPU 优化、指令重排等导致的内存访问问题(按需禁止缓存、编译优化、指令优化),从而保证 程序(尤其是多线程程序)在各种平台下对内存的访问效果一致。在这套规范中,有一个非常重要的规则——happens-before。
Happens-Before原则
原则就是:前面一个操作的结果对后续操作是可见的
程序的顺序性规则
int a = 10;//1
b = b + 1;//2
当代码执行到 2 处时,a = 10 这个结果已经是公之于众的
volatile 变量规则
volatile 保证了线程可见性。通俗讲就是如果一个线程先写了一个 volatile 变量,然后另外一个线程去读这个变量,那么这个写操作一定是 happens-before 读操作的。
传递性
这条规则是指如果 A Happens-Before B,且 B Happens-Before C,那么 A Happens-Before C。
管程中锁的规则
管程是一种通用的同步原语,在 Java 中指的就是 synchronized,synchronized 是 Java 里对管程的实现。
线程A释放锁,线程B才能获取锁执行同步语句块,此时线程B可以看到线程A释放锁之前的操作
线程 start() 规则
主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。
线程 join() 规则
这条是关于线程等待的。它是指主线程 A 等待子线程 B 完成(主线程 A 通过调用子线程 B 的 join() 方法实现),当子线程 B 完成后(主线程 A 中 join() 方法返回),主线程能够看到子线程的操作。当然所谓的“看到”,指的是对共享变量的操作。
线程中断规则
对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
对象终结规则
一个对象的初始化完成发生在它的 finalize() 方法开始前。
final
锁解决原子性问题
“同一时刻只有一个线程执行”这个条件非常重要,我们称之为互斥。
保证对共享变量的修改是互斥的,也就能保证原子性
用锁保证原子性
扩展
Java内存模型底层怎么实现的?
主要是通过内存屏障(memory barrier)禁止重排序的,即时编译器根据具体的底层体系架构,将这些内存屏障替换成具体的 CPU 指令。对于编译器而言,内存屏障将限制它所能做的重排序优化。而对于处理器而言,内存屏障将会导致缓存的刷新操作。比如,对于volatile,编译器将在volatile字段的读写操作前后各插入一些内存屏障。
volatile原理:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
- 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
- 它会强制将对缓存的修改操作立即写入主存;
- 如果是写操作,它会导致其他CPU中对应的缓存行无效。其他线程再次读取共享变量会从内存读取