CPU模型,内存分页与调优,内核与用户空间


学习此篇文章需要
先学习 java内存基础
再学习 JVM8基础结构图理解

1 CPU模型

去过机房的同学都知道,一般在大型服务器上会配置多个CPU,每个CPU还会有多个核,这就意味着多个CPU或者多个核可以同时(并发)工作

1.1 CPU Register

CPU Register也就是 CPU 寄存器CPU寄存器CPU 内部集成的,在寄存器上执行操作的效率要比在主存上高出几个数量级

CPU中至少要有六类寄存器:指令寄存器(IR)、程序计数器(PC)、地址寄存器(AR)、数据寄存器(DR)、累加寄存器(AC)、程序状态字寄存器(PSW)。这些寄存器用来暂存一个计算机字,其数目可以根据需要进行扩充

按与CPU远近来分,离得最近的是寄存器,然后缓存,最后内存。所以,寄存器是最贴近CPU的,而且CPU只与寄存器中进行存取。寄存器从内存中读取数据,但由于寄存器和内存读取速度相差太大,所以有了缓存。即读取数据的方式为:

CPU <------> 寄存器 <----> 缓存 <-----> 内存

当寄存器没有从缓存中读取到数据时,也就是没有命中,那么就从内存中读取数据

1.2 CPU Cache Memory

CPU Cache Memory也就是CPU 高速缓存。相对于硬盘读取速度来说内存读取的效率非常高,但是与 CPU 还是相差数量级,所以在 CPU 和主存间引入了多级缓存,目的是为了做一下缓冲。

CPU内部集成的缓存称为一级缓存(L1 Cache),外部的称为二级缓存(L2 Cache)。
一级缓存中又分为数据缓存(D-Cache)和指令缓存(I-Cache)。二者可以同时被CPU进行访问,减少了争用Cache所造成的冲突,提高了CPU的效能。
CPU的一级缓存通常都是静态RAM(Static RAM/SRAM),速度非常快,但是贵
二级缓存CPU性能表现的关键之一,在CPU核心不变化的情况下,增加二级缓存容量能使性能大幅度提高。而同一核心的CPU高低端之分往往也是在二级缓存上存在差异
三级缓存是为读取二级缓存后未命中的数据设计的一种缓存,在拥有三级缓存的CPU中,只有约5%的数据需要从内存中调用,这进一步提高了CPU的效率,从某种意义上说,预取效率的提高,大大降低了生产成本却提供了非常接近理想状态的性能

1.3 Main Memory

Main Memory 就是主存,主存比 L1、L2 缓存要大很多
注意:部分高端机器还有 L3 三级缓存。

内存中相关概念:

  • ROM(Read Only Memory)
    只读储存器 ,对于电脑来讲就是硬盘,在系统停止供电的时候仍然可以保持数据
  • PROM
    PROM是可编程的ROMPROMEPROM(可擦除可编程ROM)两者区别是,PROM是一次性的,也就是软件灌入后,就无法修改了,现在已经不可能使用了,而EPROM是通过紫外光的照射擦除原先的程序,是一种通用的存储器。另外一种EEPROM是通过电子擦除,价格很高,写入时间很长,写入很慢。
  • RAM(Random Access Memory)
    随机储存器 ,就是电脑内存条。用于存放动态数据。(也叫运行内存)系统运行的时候,需要把操作系统从ROM中读取出来,放在RAM中运行,而RAM通常都是在掉电之后就丢失数据,典型的RAM就是计算机的内存
  • 静态RAM(Static RAM/SRAM)
    当数据被存入其中后不会消失。SRAM速度非常快,是目前读写最快的存储设备。当这个SRAM 单元被赋予0 或者1 的状态之后,它会保持这个状态直到下次被赋予新的状态或者断电之后才会更改或者消失。需要4-6 只晶体管实现, 价格昂贵。
    一级,二级,三级缓存都是使用SRAM
  • 动态RAM(Dynamic RAM/DRAM)
    DRAM 必须在一定的时间内不停的刷新才能保持其中存储的数据。DRAM只要1 只晶体管就可以实现。
    DRAM保留数据的时间很短,速度也比SRAM慢,不过它还是比任何的ROM都要快,但从价格上来说DRAM相比SRAM要便宜很 多,
    计算机内存就是DRAM

1.4 主存存取原理

主存有两个指标:寻址(ns,纳秒级别),带宽(很大,指单位时间内字节流大小),在寻址上磁盘比内存慢了十万倍

目前计算机使用的主存基本都是随机读写存储器(RAM),现代RAM的结构和存取原理比较复杂,这里本文抛却具体差别,抽象出一个十分简单的存取模型来说明RAM的工作原理。
在这里插入图片描述
从抽象角度看,主存是一系列的存储单元组成的矩阵,每个存储单元存储固定大小的数据。每个存储单元有唯一的地址,现代主存的编址规则比较复杂,这里将其简化成一个二维地址:通过一个行地址和一个列地址可以唯一定位到一个存储单元。上图展示了一个4 x 4的主存模型。

主存的存取过程如下:

  • 当系统需要读取主存时,则将地址信号放到地址总线上传给主存,主存读到地址信号后,解析信号并定位到指定存储单元,然后将此存储单元数据放到数据总线上,供其它部件读取。
  • 写主存的过程类似,系统将要写入单元地址和数据分别放在地址总线和数据总线上,主存读取两个总线的内容,做相应的写操作。

这里可以看出,主存存取的时间仅与存取次数呈线性关系,因为不存在机械操作,两次存取的数据的“距离”不会对时间有任何影响,例如,先取A0再取A1和先取A0再取D3的时间消耗是一样的。

1.5 磁盘存取原理

在磁盘的维度里有两个指标:寻址(m/s:每秒几M)、带宽(G/M,带宽是G或者M的,指单位时间内字节流大小)

与主存不同,磁盘I/O存在机械运动耗费,因此磁盘I/O的时间消耗是巨大的。
下图是磁盘的整体结构示意图。
在这里插入图片描述

一个磁盘由大小相同且同轴的圆形盘片组成,磁盘可以转动(各个磁盘必须同步转动)。在磁盘的一侧有磁头支架,磁头支架固定了一组磁头,每个磁头负责存取一个磁盘的内容。磁头不能转动,但是可以沿磁盘半径方向运动(实际是斜切向运动),每个磁头同一时刻也必须是同轴的,即从正上方向下看,所有磁头任何时候都是重叠的(不过目前已经有多磁头独立技术,可不受此限制)。
下图是磁盘结构的示意图。
在这里插入图片描述

盘片被划分成一系列同心环,圆心是盘片中心,每个同心环叫做一个磁道,所有半径相同的磁道组成一个柱面。磁道被沿半径线划分成一个个小的段,每个段叫做一个扇区,每个扇区是磁盘的最小存储单元。为了简单起见,我们下面假设磁盘只有一个盘片和一个磁头。
当需要从磁盘读取数据时,系统会将数据逻辑地址传给磁盘,磁盘的控制电路按照寻址逻辑将逻辑地址翻译成物理地址,即确定要读的数据在哪个磁道,哪个扇区。为了读取这个扇区的数据,需要将磁头放到这个扇区上方,为了实现这一点,磁头需要移动对准相应磁道,这个过程叫做寻道,所耗费时间叫做寻道时间,然后磁盘旋转将目标扇区旋转到磁头下,这个过程耗费的时间叫做旋转时间

磁盘有磁道和扇区,一扇区 512Byte 。如果一个区域足够小,带来一个成本变大(索引)。操作系统,无论读多少数据,都是最少 4k 磁盘中拿,即:每个存储块称为一的大小

点击了解 为什么MySQL数据页是16K,系统CPU是4K

1.6 局部性原理与磁盘预读

由于存储介质的特性,磁盘本身存取就比主存慢很多,再加上机械运动耗费,磁盘的存取速度往往是主存的几百分分之一,因此为了提高效率,要尽量减少磁盘I/O。为了达到这个目的,磁盘往往不是严格按需读取,而是每次都会预读,即使只需要一个字节,磁盘也会从这个位置开始,顺序向后读取一定长度的数据放入内存。

这样做的理论依据是计算机科学中著名的局部性原理
当一个数据被用到时,其附近的数据也通常会马上被使用。程序运行期间所需要的数据通常比较集中。由于磁盘顺序读取的效率很高(不需要寻道时间,只需很少的旋转时间),因此对于具有局部性的程序来说,预读可以提高I/O效率。
预读的长度一般为页(page)的整倍数。页是计算机管理存储器的逻辑块,硬件及操作系统往往将主存和磁盘存储区分割为连续的大小相等的块,每个存储块称为一页(在许多操作系统中,页的大小通常为4k),主存和磁盘以页为单位交换数据。当程序要读取的数据不在主存中时,会触发一个缺页异常,此时系统会向磁盘发出读盘信号,磁盘会找到数据的起始位置并向后连续读取一页或几页载入内存中,然后异常返回,程序继续运行。

1.7 索引存储位置

假如文件很大了,对应得索引也会很大,以B+树索引为例解析索引和文件存储位置:
在这里插入图片描述

数据索引其实都是存储在硬盘当中的,然后这时候真正查的时候是要用到一个东西,就是在内存里边准备一个B+树,内存是速度最快的地方,所以在内存里边准备了一个B+树, B+树所有的叶子就是4k小格子。B+树其实数干是在内存里的,比如说的区间和偏移,然后这个时候如果用户想查,只要查询的时候,注意索引在where条件里,只要命中索引了,那么这个查询在B+树会走树干,最终找到某一个叶子
比如身份证号,刚好在叶子代表这个区间里,那么把它从磁盘读到内存,把它解析完之后,最笨的方法,遍历完了之后,可以知道应该下一次把哪个data page放到内存里边读进来,那么就可以找到我们那笔记录了。
由于需要从磁盘读到内存,如果把这些索引再堆到内存里的话,内存不够,存不下这些索引,所以 索引和数据都放在磁盘,内存里只存一个树干,只存一些区间
这样的话是充分利用了各自的能力,磁盘能存很多东西,然后内存速度快,然后用一种数据结构可以加快遍历的一个查找的速度,然后数据又是分而治之的存储,所以这时候获取数据速度极快,最终的目的就是为了什么?减少I/O的流量

2 内存分页

虽然Java语言的这些特点很容易“惯坏”开发人员,使得我们不需要太关心到底程序是怎么使用内存的,使用了多少内存。但是我们最好也了解Java是如何管理内存的,当我们真的遇到OutOfMemoryError时不会奇怪地问,为什么Java也有内存泄漏:要快速地知道到底什么地方导致了 OutOfMemoryError,并能根据错误日志快速地定位出错原因

2.1 物理内存与虚拟内存

2.1.1 物理内存

所谓物理内存就是我们通常所说的RAM (随机存储器)。在计算机中,还有一个存储单元叫寄存器,它用于存储计算单元执行指令(如浮点、整数等运算时)的中间结果。寄存器的大小决定了一次计算可使用的最大数值。

连接处理器和RAM或者处理器和寄存器的是地址总线,这个地址总线的宽度影响了物理地址的索引范围,因为总线的宽度决定了处理器一次可以从寄存器或者内存中获取多少个bit。同时也决定了处理器最大可以寻址的地址空间,如32位地址总线可以寻址的范围为(0x0000 0000〜0ffff ffff。这个范围是2^32=4294967296个内存位置,每个地址会引用一个字节,所以32位总线宽度可以有4GB的内存空间。

通常情况下,地址总线寄存器或者RAM有相同的位数,因为这样更容易传输数据,但是也有不一致的情况,如x8632位寄存器宽度的物理地址可能有两种大小,分别是32位物理地址和36位物理地址,拥有36位物理地址的是Pentium Pro和更高型号。

不管是在Windows系统还是Linux系统下,我们要运行程序,都要向操作系统先申请内存地址。 通常操作系统管理内存的申请空间是按照进程来管理的,每个进程拥有一段独立的地址空间,每个进程之间不会相互重合,操作系统也会保证每个进程只能访问自己的内存空间。 这主要是从程序的安全性来考虑的,也便于操作系统来管理物理内存。

其实上面所说的进程的内存空间的独立主要是指逻辑上独立,也就是这个独立是由操作系统来保证的,但是真正的物理空间是不是只能由一个进程来使用就不一定 了。因为随着程序越来越庞大和设计的多任务性,物理内存无法满足程序的需求,在这种情况下就有了虚拟内存的出现。

2.1.2 虚拟内存

虚拟内存的出现使得多个进程在同时运行时可以共享物理内存,这里的共享只是空间上共享,在逻辑上它们仍然是不能相互访问的。虚拟地址不但可以让进程共享物理内存、提高内存利用率,而且还能够扩展内存的地址空间,如一个虚拟地址可能被映射到一段物理内存、文件或者其他可以寻址的存储上。

一个进程在不活动的情况下,操作系统将这个物理内存中的数据移到一个磁盘文件中(也就是通常windows系统上的页面文件,或者Linux系统上的SWAP交换分区),而真正高效的物理内存留给正在活动的程序使用。在这种情况下,在我们重新唤醒一个很长时间没有使用的程序时,磁盘会吱吱作响,并且会有一个短暂的停顿得到印证,这时操作系统又会把磁盘上的数据重新交互到物理内存中。但是我们必须要避免这种情况的经常出现,如果操作系统频繁地交互物理内存的数据和磁盘数据,则效率将会非常低,尤其是在Linux服务器上,我们要关注Linux中swap的分区的活跃度。
如果swap分区被频繁使用,系统将会非常缓慢,很可能意味着物理内存已经严重不足或者某些程序没有及时释放内存。

Linux分区中SWAP意思是交换,顾名思义,当某进程向OS请求内存发现不足时,OS会把内存中暂时不用的数据交换出去,放在SWAP分区中,这个过程称为SWAP OUT。当某进程又需要这些数据且OS发现还有空闲物理内存时,又会把SWAP分区中的数据交换回物理内存中,这个过程称为SWAP IN

点击了解Linux分区(目录中选择分区)

2.2 内存分页大小对性能提升原理

首先,我们需要回顾一小部分计算机组成原理,这对理解大内存分页至于JVM性能的提升是有好处的。

2.2.1 什么是内存分页

我们知道,CPU是通过寻址来访问内存的。32CPU的寻址宽度是 0~0xFFFFFFFF ,计算后得到的大小是4G,也就是说可支持的物理内存最大是4G。但在实践过程中,碰到了这样的问题,程序需要使用4G内存,而可用物理内存小于4G,导致程序不得不降低内存占用。
为了解决此类问题,现代CPU引入了 MMUMemory Management Unit 内存管理单元)

MMU的核心思想是利用虚拟地址替代物理地址,即CPU寻址时使用虚址,由MMU负责将虚址映射为物理地址。MMU的引入,解决了对物理内存的限制,对程序来说,就像自己在使用4G内存一样。

内存分页(Paging)是在使用MMU的基础上,提出的一种内存管理机制。它将虚拟地址物理地址按固定大小(4K)分割成页(page)和页帧(page frame),并保证 页与页帧的大小相同,这种机制,从数据结构上,保证了访问内存的高效,并使OS能支持非连续性的内存分配。在程序内存不够用时,还可以将不常用的物理内存页转移到其他存储设备上,比如磁盘,这就是大家耳熟能详的虚拟内存。
在这里插入图片描述
页表其实是一个数组用于把虚拟页号物理帧号对应起来
其索引为VPN(虚拟地址) 索引值对应的项为PTE(页表项) 项中的值为PFN(物理页帧),每个PTE中还有很多别的内容,比如有很多不同的位:有效位、保护位、存在位、脏位、参考位

虚拟地址物理地址需要通过映射,才能使CPU正常工作。而映射就需要存储映射表。在现代CPU架构中,映射关系通常被存储在物理内存上一个被称之为页表(page table)的地方。
如下图:
在这里插入图片描述

从这张图中,可以清晰地看到CPU与页表,物理内存之间的交互关系。

由于页表是被存储在内存中的。我们知道CPU通过总线访问内存,肯定慢于直接访问寄存器的。为了进一步优化性能,现代CPU架构引入了TLBTranslation lookaside buffer页表寄存器缓冲),用来缓存一部分经常访问的页表内容。
如下图:
在这里插入图片描述

2.2.2 为什么要支持大内存分页

TLB是有限的,这点毫无疑问。当超出TLB的存储极限时,就会发生 TLB miss,之后,OS就会命令CPU去访问内存上的页表。如果频繁的出现TLB miss,程序的性能会下降地很快。
为了让TLB可以存储更多的页地址映射关系,我们的做法是调大内存分页大小。
如果一个页4M,对比一个页4K,前者可以让TLB多存储1000个页地址映射关系,性能的提升是比较可观的。

2.3 调整OS和JVM内存分页

Linuxwindows下要启用大内存页,有一些限制和设置步骤。

2.3.1 Linux

Linux限制:需要2.6内核以上或2.4内核已打大内存页补丁。
确认是否支持,请在终端敲如下命令:

# cat /proc/meminfo | grep Huge
HugePages_Total: 0
HugePages_Free: 0
Hugepagesize: 2048 kB

如果有HugePage字样的输出内容,说明OS是支持大内存分页的。Hugepagesize就是默认的大内存页size
接下来,为了让JVM可以调整大内存页size,需要设置下OS 共享内存段最大值和大内存页数量。

2.3.1.1 共享内存段最大值

建议这个值大于Java Heap size,这个例子里设置了4G内存。

# echo 4294967295 > /proc/sys/kernel/shmmax
2.3.1.2 大内存页数量
# echo 154 > /proc/sys/vm/nr_hugepages

这个值一般是 Java进程占用最大内存/单个页的大小 ,比如java设置 1.5G,单个页 10M,那么数量为 1536/10 = 154。
注意:因为proc是内存FS,为了不让设置在重启后被冲掉,建议写个脚本放到 init 阶段(rc.local)。

2.3.2 Windows

Windows限制:仅支持 windows server 2003 以上server版本
操作步骤:

  1. Control Panel -> Administrative Tools -> Local Security Policy
  2. Local Policies -> User Rights Assignment
  3. 双击Lock pages in memory, 添加用户和组
  4. 重启电脑

注意: 需要管理员操作。

2.3.3 单个页大小调整

JVM启用时加参数 -XX:LargePageSizeInBytes=10m
如果JDK是在1.5 update5以前的,还需要手动加 -XX:+UseLargePages,作用是启用大内存页支持。

2.4 大内存分页的副作用

因为每页size变大了,导致JVM在计算Heap内部分区(perm, new, old)内存占用比例时,会出现超出正常值的划分。最坏情况下是,某个区会多占用一个页的大小。不过后续jvm版本也在调整这个策略。
一般情况,不建议将页size调得太大,4-64M,是可以接受的(默认是4M)

2.5 Fork

2.5.1 fork创建子进程特点

  • 父子进程fork调用后,会创建一个新的子进程,该子进程与父进程几乎完全相同,包括代码、数据和打开文件等。子进程从fork调用的位置开始执行,父进程和子进程在fork调用之后的代码处继续执行。
  • 资源继承:子进程继承了父进程的大部分资源,包括打开的文件、文件描述符、信号处理器等。但是有些资源(如互斥锁和定时器)可能需要进行特殊处理,以避免竞争条件或资源泄漏。
  • 内存:父进程和子进程拥有独立的虚拟内存空间,每个进程都有自己的内存映射表。子进程通过写时复制(copy-on-write)机制与父进程共享物理内存,只有在需要修改内存内容时才会进行复制。
  • 父子关系:父进程可以通过fork的返回值判断是否为子进程。父进程的fork返回子进程的PID,而子进程的fork返回0。这样可以根据返回值的不同,在父子进程中执行不同的逻辑。

2.5.2 Copy On Write

在创建子进程的过程中,操作系统会把父进程的页表复制一份给子进程,这个页表记录着虚拟地址物理地址映射关系,而不会复制物理内存,也就是说,两者的虚拟空间不同,但其对应的物理空间是同一个。
在这里插入图片描述

这样一来,子进程就共享了父进程的物理内存数据了,这样能够节约物理内存资源,页表对应的页表项的属性会标记该物理内存的权限为只读。

当父进程或者子进程在向共享内存发起写操作时,CPU 就会触发写保护中断,这个写保护中断是由于违反权限导致的,然后操作系统会在写保护中断处理函数里进行物理内存的复制,并重新设置其内存映射关系,将父子进程的内存读写权限设置为可读写,最后才会对内存进行写操作,这个过程被称为写时复制(Copy On Write)
在这里插入图片描述

写时复制顾名思义,在发生写操作的时候,操作系统才会去复制物理内存,这样是为了防止 fork 创建子进程时,由于物理内存数据的复制时间过长而导致父进程长时间阻塞的问题。

3 内核空间与用户空间

3.1 定义

一个计算机通常有一定大小的内存空间,如使用的计算机是4GB的地址空间,但是程序并不能完全使用这些地址空间,因为这些地址空间被划分为内核空间用户空间。程序只能使用用户空间的内存,这里所说的使用是 指程序能够申请的内存空间 ,并不是程序真正访问的地址空间。
内核空间主要是指操作系统运行时所使用的用于程序调度、虚拟内存的使用或者连接硬件资源等的程序逻辑。

  • 用户空间(User Mode):是指应用程序运行时的执行环境。在用户态下,应用程序只能访问受限资源,如应用程序自身的内存空间、CPU 寄存器等,并且不能直接访问操作系统的底层资源和硬件设备。
    非内核应用程序运行在用户空间,用户空间中的代码运行在较低的特权级别上
  • 内核空间(Kernel Mode):是指操作系统内核运行时的执行环境。在内核态下,操作系统具有更高的权限,可以直接访问系统的硬件和底层资源,如 CPU、内存、设备驱动程序等。
    操作系统的核心,是操作系统工作的基础,它负责管理系统的进程、内存、设备驱动程序、文件和网络系统,决定着系统的性能和稳定性

3.2 为何需要内核空间和用户空间的划分

由于每个进程都独立使用属于自己的内存一样,为了保证操作系统的稳定性,运行在操作系统中的 用户程序不能访问操作系统所使用的内存空间。这也是从安全性上考虑的,如访问硬件资源只能由操作系统来发起,用户程序不允许直接访问硬件资源。如果用户程序需要访问硬件资源,如网络连接等,.可以调用操作系统提供的接口来实现,这个调用接口的过程也就是系统调用。

每一次系统调用都会存在两个内存空间的切换,通常的网络传输也是一次系统调用,通过网络传输的数据先是从内核空间接收到远程主机的数据,然后再从内核空间复制到用户空间,供用户程序使用。这种从内核空间到用户空间的数据复制很费时,虽然保住了程序运行的安全性和稳定性,但是也牺牲了一部分效率。但是现在已经出现了很多其他技术能够减少这种从内核空间到用户空间的数据复制的方式,如Linux系统提供了 sendflle文件传输方式。

内核空间和用户空间的大小如何分配也是一个问题,是更多地分配给用户空间供用户程序使用,还是首先保住内核有足够的空间来运行,这要平衡一下。如果是一台登录服务器,很显然,要分配更多的内核空间,因为每一个登录用户操作系统都会初始化一个用户进程,这个进程大部分都在内核空间里运行。在当前的 Windows 32位操作系统中默认内核空间和用户空间的比例是1:1 (2GB的内核空间,2GB的用户空间),而在32位Linux系统中默认的比例是1:3 (1GB的内核空间,3GB的用户空间)

3.3 内核用户空间切换的影响

java的线程是映射到操作系统原生线程之上的,如果要阻塞或唤醒一个线程就需要操作系统介入,需要在用户态核心态之间切换,这种切换会消耗大量的系统资源,因为用户态内核态都有各自专用的内存空间,专用的寄存器等,用户态切换至内核态需要传递给许多变量、参数给内核,内核也需要保护好用户态在切换时的一些寄存器值、变量等,以便内核态调用结束后切换回用户态继续工作。
如果线程状态切换是一个高频操作时,这将会消耗很多CPU处理时间

3.4 单核多核cpu与多线程

3.4.1 单核CPU为何也支持多线程呢

多任务系统往往需要同时执行多道作业。作业数往往大于机器的CPU数,然而一颗CPU同时只能执行一项任务,如何让用户感觉这些任务正在同时进行呢? 操作系统的设计者巧妙地利用了时间片轮转的方式

时间片是CPU分配给各个任务(线程)的时间

线程上下文是指某一时间点CPU寄存器程序计数器(PC寄存器)的内容,CPU通过时间片分配算法来循环执行任务(线程),因为时间片非常短,所以CPU通过不停地切换线程执行。
换言之,单CPU这么频繁,多核CPU一定程度上可以减少上下文切换

3.4.2 超线程

现代CPU除了处理器核心之外还包括寄存器、L1L2缓存这些存储设备、浮点运算单元、整数运算单元等一些辅助运算设备以及内部总线等。一个多核的CPU也就是一个CPU上有多个处理器核心,就意味着程序的不同线程需要经常在CPU之间的外部总线上通信,同时还要处理不同CPU之间不同缓存导致数据不一致的问题。

超线程这个概念是Intel提出的,简单来说是在一个CPU上真正的并发两个线程,由于CPU都是分时的(如果两个线程A和B,A正在使用处理器核心,B正在使用缓存或者其他设备,那AB两个线程就可以并发执行,但是如果AB都在访问同一个设备,那就只能等前一个线程执行完后一个线程才能执行)。实现这种并发的原理是 在CPU里加了一个协调辅助核心,根据Intel提供的数据,这样一个设备会使得设备面积增大5%,但是性能提高15%~30%

3.4.3 线程上下文切换

线程上下文:指某一时间点CPU寄存器程序计数器(PC寄存器)的内容

线程上下文切换:

  • 线程切换,同一进程中的两个线程之间的切换
  • 进程切换,两个进程之间的切换
  • 模式切换,在给定线程中,用户模式和内核模式的切换
  • 地址空间切换,将虚拟内存切换到物理内存

CPU切换前把当前任务的状态保存下来,以便下次切换回这个任务时可以再次加载这个任务的状态,然后加载下一任务的状态并执行。任务的状态保存及再加载, 这段过程就叫做上下文切换

每个线程都有一个程序计数器(记录要执行的下一条指令),一组寄存器(保存当前线程的工作变量),堆栈(记录执行历史,其中每一帧保存了一个已经调用但未返回的过程)。

程序计数器是一个专用的寄存器,用于表明指令序列中 CPU 正在执行的位置,存的值为正在执行的指令的位置或者下一个将要被执行的指令的位置。

  • 挂起当前任务(线程/进程),将这个任务在 CPU 中的状态(上下文)存储于内存中的某处
  • 恢复一个任务(线程/进程),在内存中检索下一个任务的上下文并将其在 CPU 的寄存器中恢复
  • 跳转到程序计数器所指向的位置(即跳转到任务被中断时的代码行),以恢复该进程在程序中]
    在这里插入图片描述

问题:线程运行过程中申请到的东西在切换时是否全部要保存,比如线程中有个循环,或者声明了很多对象,这些是否都要保存,也存在线程私有区吗

在线程切换时,操作系统会保存线程的上下文信息,包括线程的程序计数器、寄存器值、堆栈指针等。这些信息可以用来恢复线程的执行状态。
对于线程中的循环或声明的对象,它们的状态在线程切换时不会自动保存。线程中的循环会根据程序计数器的值来确定下一条要执行的指令,而声明的对象会存在线程的堆栈中。当线程切换时,这些状态不会被保存,而是由新的线程重新执行。
然而,每个线程都有自己的线程栈,用于保存局部变量和函数调用的信息。这个线程栈是线程私有的,不会被其他线程访问到。因此,线程中的局部变量和函数调用信息是在线程切换时自动保存的,无需额外的操作。
需要注意的是,如果多个线程并发访问共享资源,比如全局变量或共享对象,那么在切换时可能需要通过同步机制来保证数据的一致性。否则,可能会导致数据竞争和不确定的结果。

3.4.4 线程上下文切换的影响

线程上下文切换会导致额外的开销,常常表现为高并发执行时速度会慢串行,因此减少上下文切换次数便可以提高多线程程序的运行效率。

  • 直接消耗:指的是CPU寄存器需要保存和加载, 系统调度器的代码需要执行, TLB实例需要重新加载, CPU 的pipeline需要刷掉
  • 间接消耗:指的是多核的cache之间得共享数据, 间接消耗对于程序的影响要看线程工作区操作数据的大小

3.4.5 引起线程上下文切换的因素

引起线程上下文切换的因素:

  • 当前执行任务(线程)的时间片用完之后,系统CPU正常调度下一个任务
  • 中断处理,在中断处理中,其他程序打断了当前正在运行的程序。当CPU接收到中断请求时,会把正在运行的程序和发起中断请求的程序之间进行一次上下文切换。中断分为硬件中断软件中断,软件中断包括因为IO阻塞、未抢到资源或者用户代码等原因,线程被挂起。
  • 用户态切换,对于一些操作系统,当进行用户态切换时也会进行一次上下文切换,虽然这不是必须的。
  • 多个任务抢占锁资源,在多任务处理中,CPU会在不同程序之间来回切换,每个程序都有相应的处理时间片,CPU在两个时间片的间隔中进行上下文切换

因此优化手段有:

  • 无锁并发编程,多线程处理数据时,可以用一些办法来避免使用锁,如将数据的ID按照Hash取模分段,不同的线程处理不同段的数据
  • CAS算法,Java的Atomic包使用CAS算法来更新数据,而不需要加锁
    使用最少线程
  • 协程,单线程里实现多任务的调度,并在单线程里维持多个任务间的切换

合理设置线程数目既可以最大化利用CPU,又可以减少线程切换的开销。

  • 高并发,低耗时的情况,建议少线程。
  • 低并发,高耗时的情况:建议多线程。
  • 高并发高耗时,要分析任务类型、增加排队、加大线程数

3.4.6 切换查看

Linux系统下可以使用vmstat命令来查看上下文切换的次数, 其中cs列就是指上下文切换的数目(一般情况下, 空闲系统的上下文切换每秒大概在1500以下)
在这里插入图片描述

3.4.7 Java线程调度

Java线程调度:

  • 抢占式调度
    指的是每条线程执行的时间、线程的切换都由系统控制,系统控制指的是在系统某种运行机制下,可能每条线程都分同样的执行时间片,也可能是某些线程执行的时间片较长,甚至某些线程得不到执行的时间片。在这种机制下,一个线程的堵塞不会导致整个进程堵塞。
    java使用的线程调度使用抢占式调度,Java中线程会按优先级分配CPU时间片运行,且优先级越高越优先执行,但优先级高并不代表能独自占用执行时间片,可能是优先级高得到越多的执行时间片,反之,优先级低的分到的执行时间少但不会分配不到执行时间。
  • 协同式调度
    指某一线程执行完后主动通知系统切换到另一线程上执行,这种模式就像接力赛一样,一个人跑完自己的路程就把接力棒交接给下一个人,下个人继续往下跑。线程的执行时间由线程本身控制,线程切换可以预知,不存在多线程同步问题,但它有一个致命弱点:如果一个线程编写有问题,运行到一半就一直堵塞,那么可能导致整个系统崩溃。

3.4.8 进程切换和线程切换

进程切换和线程切换区别:

  • 进程切换:进程切换涉及到更多的内容,包括整个进程的地址空间、全局变量、文件描述符等。因此,进程切换的开销通常比线程切换大。
  • 线程切换:线程切换只涉及到线程的堆栈、寄存器和程序计数器等,不涉及进程级别的资源,因此线程切换的开销较小。

3.4.9 为什么并发执行线程要加锁

并发执行线程需要加锁主要是为了保护共享数据,防止出现竞态条件

竞态条件是指当多个线程同时访问和操作同一块数据时,最终结果依赖于线程的执行顺序,这可能导致数据的不一致性。
通过加锁,我们可以确保在任何时刻只有一个线程能够访问共享数据,从而避免竞态条件,确保数据的一致性和完整性。

3.5 Windows和Linux的CPU竞争方式

CPU竞争(CPU contention)是指多个进程线程在同一时间竞争使用有限的CPU资源。在多任务操作系统中,CPU竞争是很常见的现象。操作系统的任务调度器负责管理这些竞争,确保所有进程和线程都能得到合理的CPU时间。WindowsLinux作为两种不同的操作系统,它们在处理CPU竞争方面有一些区别:

  • 调度算法
    WindowsLinux使用不同的任务调度算法。Windows使用优先级驱动的抢占式多任务调度器,它根据进程的优先级来分配CPU时间。
    Linux使用完全公平调度器(CFS),它根据进程的虚拟运行时间来分配CPU时间,以实现公平的资源分配
  • 优先级WindowsLinux的进程优先级有所不同。Windows0-31个优先级级别,其中0-15动态优先级16-31实时优先级Linux-2019的优先级范围,其中-20表示最高优先级19表示最低优先级。这些优先级值在两个系统中的含义和作用也有所不同。
  • 实时调度WindowsLinux都支持实时调度,但实现方式不同。
    Windows使用实时优先级(16-31)来支持实时任务,这些任务具有比普通任务更高的优先级。
    Linux使用SCHED_FIFOSCHED_RR两种实时调度策略,它们分别对应先进先出和轮询调度算法。
  • 多处理器支持WindowsLinux都支持多处理器系统,但在处理器亲和性和负载均衡方面有所不同。
    Windows使用处理器亲和性掩码来指定进程可以在哪些处理器上运行,而Linux使用cpuset来实现类似的功能。在负载均衡方面,WindowsLinux都有各自的策略和算法。
  • 用户界面WindowsLinux在用户界面方面也有所不同。
    Windows提供了图形化的任务管理器,用户可以通过这个工具查看和管理系统中的进程和资源使用情况。Linux通常使用命令行工具,如tophtop,来查看和管理进程,但也有图形化的系统监视器工具可供选择。
  • 0
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值