1. Java并发编程-系统篇

155 阅读6分钟

a要想学好并发编程,深入理解多线程运行的原理,离不开对操作系统知识的理解。所以我打算先以一篇系统篇,让大家了解系统架构中可能存在的一些问题。

CPU指令结构

CPU中的工作组件有:控制单元、运算单元、数据单元。

控制单元

控制单元是CPU的指挥中心,由指令寄存器、指令译码器、指令计数器、操作控制器等组成。它负责指令的读取和分析,并对整个CPU进行协调工作。但线程开始执行的时候,控制单元会从存储器中读取指令存入指令寄存器中,并通过指令译码器对指令进行分析并进行相应的操作。然后通过操作控制器,顺序执行指令。

运算单元

运算单元(ALU),可以用来进行算术运算和逻辑运算。本身接受控制单元的命令控制,完成指令中的算数运算部分。

存储单元

存储单元中包括CPU的多级缓存L1、L2和L3,及寄存器组,它们是CPU存储数据的地方。线程执行过程中读取的指令和数据,会经过L3 -> L2 -> L1 -> 寄存器的顺序进行存储。

CPU多级缓存

从上述的存储单元中,我们知道CPU存在多级缓存结构L1、L2、L3。那么我们可以思考,CPU设计这么多层缓存,是为了什么呢?

在计算机的发展中,人们发现,内存的读取速度远远不及CPU的执行速度。这会让CPU去读取存储器的时候,浪费了大量的时间。所以CPU厂商在CPU中内置了缓存存储器,越接近CPU的缓存组件,CPU从中读取的速度越快。所以就有了以下的CPU多级缓存架构。

  • L3: 在多个CPU Core中共享数据,在多级缓存中离CPU最远,可以存储的数据也是最大。
  • L2: 单个CPU Core独享数据,是CPU缓存架构中的第二级缓存。
  • L1:离CPU寄存器最近,读写速度最快。分为数据缓存和指令缓存。

而缓存中的最小存储区块时缓存行,一般为64bytes大小。

了解了CPU的多层缓存架构之后,再来思考那个问题,CPU为什么要设计多层缓存架构?

其实是基于空间局部性和时间局部性原理来考虑的。

空间局部性原理

即程序访问一部分数据,而这部分数据的相邻数据在一段时间内也可能被访问,CPU会将这些数据一次性加载进来。

我引用一段代码来解释空间局部性原理。

for(i = 0; i <= N ;i++){ a[i] = b[i] + c[i]; }

CPU访问b[0]时,将b[1]~b[t]这部分数据一次性加载到缓存中,来避免多次访问内存。

时间局部性原理

即一块数据正在被访问,那么近期有可能被再次访问。

对于时间局部性原理的应用,我简单理解为: 如果数据在一段时间内会被频繁访问,那么我们可以考虑将这部分数据进行缓存来达到加速访问的目的。

CPU运行等级和保护机制

在使用操作系统的过程中,你可否想过,内存的数据我们可否进行直接修改?或者说,有什么办法可以操作内存修改数据呢?

基于x86提供了分层的权限限制,把进程的运行分为4个级别,分别为:ring0、ring1、ring2、ring3。这四个权限大小顺序是 ring0>ring1>ring2>ring3

一般的操作系统如Linux和Window,只存在两个级别:ring0和ring3。

操作系统利用这个分层级别的特性,把能够访问到关键资源如IO、内存等代码放在了ring0,这部分我们称为内核态。而把普通的程序代码放到了ring3,这部分我们称为用户态。分层级别有利于保护我们的操作系统,避免用户随意访问内核资源,造成系统奔溃。

而我们的用户空间内存资源都是由操作系统提过的系统调用进行分配的,用户无法直接操作内存进行自我分配,必须交由内核去完成。

那么用户想要使用系统资源时,该怎么办呢?

操作系统为我们提供了规范性的系统函数供我们调用。当我们的调用系统函数时,会发生CPU从用户态切换为内核态。而当调用返回时,CPU又会从内核态切换回用户态。所以当我们在应用中执行如channel.read()等访问IO或内存的系统调用函数时,会发生两次进程上下文切换,这个过程会造成线程阻塞。

线程管理

在现代计算机中,支持在进程中创建多线程来运行任务。那么线程到底是什么呢?它是操作系统内核创建的,还是由用户进程自己创建的呢?

随着CPU的高速发展,单进程单线程已经不能满足任务处理的需求。那么就需要有多线程来充分利用CPU资源进行任务并行处理。而线程是进程执行程序的最小单元。

线程可分为用户级线程和内核级线程。

用户级线程

用户级线程是由应用程序自行创建的,对于CPU来说,是感知不到用户级线程的存在,它只存在于用户内存空间,由用户进程维护线程表,它的创建、销毁都不需要使用系统调用,也就不会产生用户态和内核态的切换了。但是,系统调用造成线程阻塞时,会导致进程阻塞。

内核级线程

内核级线程由操作系统进行创建、销毁,应用程序不对线程进行管理。内核会维护一张线程表,管理线程的状态和上下文信息。当调用系统调用时引起的线程阻塞不会影响进程其他线程的运行。

总结

操作系统的一些关于CPU、线程等基础知识我们就讲到这,通过对上述的理解,我们可以知道平日里息息相关的多线程程序离不开CPU资源的调度。CPU通过分配时间片来调度线程的任务执行,我们也知道了线程的创建、销毁都会通过系统调用交由内核来完成,这个过程会发生用户态和内核态的切换,而使用系统调用的同时,会造成当前执行线程的阻塞。

通过对本文的理解,有几个问题供大家思考:

1. 两条线程运行在不同的CPU Core, 当他们访问内存中同一个变量的时候,会有什么样的问题?如果有,那么有什么样的解决方案?

2. 通过对两种线程模型的理解,你觉得JVM使用的是哪种线程模型呢?

3. 在你的项目开发中,你都通过什么样的方式查看CPU的使用情况?