1 CPU和内存
CPU和内存 是核心中的核心。 先上图:
一个CPU会有单个、或多个核。每个核都自己的寄存器、PC、ALU、缓存等。多个核之间共享L3缓存。与内存通过总线来传输。
晶体管组成逻辑门电路,CPU是几十亿个这样的逻辑门组成。能够非常快速的实现计算。 CPU:我只管做事,你只要输入数据,然后要我怎么做我很快就告诉你结果。但是如果你想让我自动的去做事,那么就需要内存了。 写程序就是在往内存存入指令(程序)和数据。CPU的本质很简单。就是不断的取指令做运算(通过逻辑电路),一直取到指令结束为止。
CPU根据PC(下一条指令的地址)从内存拿到数据,先存到寄存器。CPU有几十个(具体多少不用纠结)不同功能的寄存器。存到寄存器后,ALU(Arithmetic&logical Unit 逻辑运算单元)才开始真正的计算,就是用来逻辑处理的。 计算完成在写入到内存中去。 内存也叫主存。
单核CPU一次只能执行一个线程,切线程时需要把数据存到缓存cache、内存保存好。然后寄存器再去加载线程2的指令和数据。
超线程:四核八线程,即一个核对应两个pc、registers。 每一个CPU的核一个ALU 对应多个PC和Registers。 两个线程同时运行的时候,运算只通过ALU在PC和registers中(ALU非常非常快)去切换,避免线程切换导致的内存切换。
CPU和内存之间通过总线来通信。总线分为很多种。有地址总线、数据总线。
2 缓存行
cache line 缓存行 大小是64 Byte。如果总线宽是32则是 16个总线单位长;如果是64那就是8个总线长。 在内存中分好的。 CPU每一次读取一个cache line,然后存到L3,在存到L2,在存到L1,然后在到寄存器进行计算。
这也就解释了,为什么连续的存储空间数组比链表可以让CPU更高效率的执行!
2.1 缓存一致性协议 MESI Cache。
- m: modified 已修改
- e:exclude 独自拥有
- s:shared 共享,但都不改
- i:invalid 无效的
这是为了解决多核CPU间L1 L2缓存间的cache line数据不同步问题。 所以volatile是用来保证缓存一致性的。
如果变量x是volitale修饰的?
CPU1修改了缓存行中的数据x,那么会立即同步到主存,也就是内存。同时通过硬件缓存锁(或者总线锁)来通知CPU2,CPU2在读取最新数据到本身的L1/L2缓存中,最终到寄存器中。 注意: 单核CPU是不存在数据不可见的问题的。
但这会引发一个问题:
如果x和y都被volitale修饰,并且在同一个cache line中。而两个CPU分别读取了这x和y,那么会出现互相通知的情况,导致CPU被不断的中断刷新缓存和寄存器。从而性能下降。
那怎么解决呢?
通过在前后各添加7个long型的数据(56位),然后加上自己本身的long。总共就是64位,那么无论怎么分cache line,该数据都不会被分到同一行。
3 指令重排序
CPU对于指令是乱序执行的。 假设有指令1和指令2,当CPU执行指令1的时候(指令1是去内存读取数据比较慢,如读取IO数据或去内存读数据),此时CPU处于读等待状态,为了不浪费性能,CPU会检测指令2 ,如果指令2跟指令1没有依赖关系,那么会执行指令2。从而出现指令2比指令1先执行了。
本质就是一系列有顺序的指令。但是其中有的指令之间没有依赖关系,是可以在读等待装填去执行的。出现了并发乱序执行。
从java层看似一个操作,实则到汇编指令的时候是分了三步走的。
java建立对象的汇编指令Object t= new Object();分三步走:
- 分配内存
- 初始化
- 建立关联
4 原码、补码、反码
首先要正确理解他们的作用!
计算机存储和识别的都是补码。 为什么?因为计算机只想做加法操作。不管是减法还是乘法、除法,我都通过加法规则来完成。
但是,如果是负数,那么补码就不是它表面看起来的值。
例如:有符号数:111 按照我们第一印象就是1负数,11是值,所以这个数是-3。 这样理解是错误的!
我们要知道,这是计算机的补码。真正的数值是多少计算机内部是知道的,但是我们此时看不出来!
因此,需要转成原码,然后计算成十进制。 补码111-->反码是110-->原码是001-->1-->添加负号:-1。 所以,真正的值是-1。 具体转换规则是怎么样的,接着往下看。
因此,原码和反码都只是工具,都是为了推导出补码而存在的。计算机根本不管这个!
此外,原码、补码都是针对负数才有意义!正数根本不需要!
原码:方便给人计算,查看真正的值。
反码:推导过程的中间一环而已(对原码除符号位外,取反得到反码)
补码:计算机真正需要的东西,只能识别它!存储它!(对反码+1得到补码)
4.1 原码到补码的过程
- +1
- 除了符号位,按位取反
4.1 补码到原码的过程
- -1
- 按位取反:
- 转换成十进制
- 添加负号
4.1 一个Byte表示的范围是多少
- 无符号范围: 0~ 255
- 有符号范围:-128~ 127
解答过程:
一个Byte是8位,也就是 0000 0000 ~ 1111 1111
无符号简单:
- 0~(2^7+2^6+2^5++2^4+2^3+2^2+2^1+2^0)
- => 0~255。
有符号正数,符号位为0表示正数:
我们需要将补码转成原码: 不变。
- 范围: 0 000 0000~ 0111 1111
- => 0~(2^6+2^5++2^4+2^3+2^2+2^1+2^0)
- =>0~ 127。
有符号负数,符号位为1表示负数:
我们需要将补码转成原码:
范围: 1 000 0000~ 1111 1111
首先对1 000 0000进行转换:
- -1操作:0111 1111
- 取反: 1000 0000
- 十进制: 2^7=128
- 添加负号:-128
再对1111 1111进行转换:
- -1操作:1111 1110
- 取反: 0000 0001
- 十进制: 1
- 添加负号:-1
因此,负数范围为-128~ 1。
整个范围为: -128~ 127(也就是-2^7 ~ 2^7-1)
4.2 一个int表示的范围是多少
同上推到过程,范围为:-2^31 ~ 2^31 -1。