重庆列举网 > 教育培训 > 其他教育培训 > 计算机程序设计员是做什么的需要准备什么难不难重庆哪里可以考
重庆
[切换城市]

计算机程序设计员是做什么的需要准备什么难不难重庆哪里可以考

更新时间:2020-04-29 15:41:11 浏览次数:69次
区域: 重庆 > 渝中 > 七星岗
详情联系:朱老师 18170060794 微信同号

高德纳(D. E. Knuth)教授是备受尊崇的系列巨著《计算机程序设计艺术》(The Art of Computer Programming)和数十篇受到高度赞誉的计算机科学的作者。2011年6月,结束了在英国的书籍研讨和系列演讲的高德纳教授,跟BCS编辑Justin Richards畅谈了自己的人生和工作。dunsijiaoyu zz

线程同步出现的根本原因是访问公共资源需要多个操作,而这多个操作的执行过程不具备原子性,被任务调度器分开了,而其他线程会破坏共享资源,所以需要在临界区做线程的同步,这里我们先明确一个概念,就是临界区,他是指多个任务访问共享资源如内存或文件时候的指令,他是指令并不是受访问的资源。
IX 定义了五种同步对象,互斥锁,条件变量,自旋锁,读写锁,信号量,这些对象在 JVM 中也都有对应的实现,并没有全部使用 IX 定义的 api,通过 Java 实现灵活性更高,也避免了调用native方法的性能开销,当然底层终都依赖于 pthread 的 互斥锁 mutex 来实现,这是一个系统调用,开销很大,所以 JVM 对锁做了自动升降级,基于AQS的实现以后在分析,这里主要说一下关键字 synchronized 。
当声明 synchronized 的代码块时,编译而成的字节码会包含一个 monitorenter 和 多个 monitorexit (多个退出路径,正常和异常情况),当执行 monitorenter 的时候会检查目标锁对象的计数器是否为0,如果为0则将锁对象的持有线程设置为自己,然后计数器加1,获取到锁,如果不为0则检查锁对象的持有线程是不是自己,如果是自己就将计数器加1获取锁,如果不是则阻塞等待,退出的时候计数器减1,当减为0的时候清楚锁对象的持有线程标记,可以看出 synchronized 是支持可重入的。
刚刚说到线程的阻塞是一个系统调用,开销大,所以 JVM 设计了自适应自旋锁,就是当没有获取到锁的时候, CPU 回进入自旋状态等待其他线程释放锁,自旋的时间主要看上次等待多长时间获取的锁,例如上次自旋5毫秒没有获取锁,这次就6毫秒,自旋会导致 CPU 空跑,另一个就是的锁机制,因为该线程自旋获取到锁,而其他正在阻塞的线程还在等待。除了自旋锁, JVM 还通过 CAS 实现了轻量级锁和偏向锁来分别针对多个线程在不同时间访问锁和锁仅会被一个线程使用的情况。后两种锁相当于并没有调用底层的信号量实现(通过信号量来控制线程A释放了锁例如调用了 wait(),而线程B就可以获取锁,这个只有内核才能实现,后面两种由于场景里没有竞争所以也就不需要通过底层信号量控制),只是自己在用户空间维护了锁的持有关系,所以更。
程序设计的5个底层逻辑,决定你能走多快
如上图所示,如果线程进入 monitorenter 会将自己放入该 objectmonitor 的 entryset 队列,然后阻塞,如果当前持有线程调用了 wait 方法,将会释放锁,然后将自己封装成 objectwaiter 放入 objectmonitor 的 waitset 队列,这时候 entryset 队列里的某个线程将会竞争到锁,并进入 active 状态,如果这个线程调用了 notify 方法,将会把 waitset 的个 objectwaiter 拿出来放入 entryset (这个时候根据策略可能会先自旋),当调用 notify 的那个线程执行 moniterexit 释放锁的时候, entryset 里的线程就开始竞争锁后进入 active 状态。
为了让应用程序免于数据竞争的干扰, Java 内存模型中定义了 happen-before 来描述两个操作的内存可见性,也就是 X 操作 happen-before 操作 Y , 那么 X 操作结果 对 Y 可见。
JVM 中针对 volatile 以及 锁 的实现有 happen-before 规则, JVM 底层通过插入内存屏障来限制编译器的重排序,以 volatile 为例,内存屏障将不允许 在 volatile 字段写操作之前的语句被重排序到写操作后面 , 也不允许读取 volatile 字段之后的语句被重排序带读取语句之前。插入内存屏障的指令,会根据指令类型不同有不同的效果,例如在 monitorexit 释放锁后会强制刷新缓存,而 volatile 对应的内存屏障会在每次写入后强制刷新到主存,并且由于 volatile 字段的特性,编译器无法将其分配到寄存器,所以每次都是从主存读取,所以 volatile 适用于读多写少得场景,好只有个线程写多个线程读,如果频繁写入导致不停刷新缓存会影响性能。
关于应用程序中设置多少线程数合适的问题,我们一般的做法是设置 CPU 大核心数 * 2 ,我们编码的时候可能不确定运行在什么样的硬件环境中,可以通过 Runtime.getRuntime().availableProssors() 获取 CPU 核心。
但是具体设置多少线程数,主要和线程内运行的任务中的阻塞时间有关系,如果任务中全部是计算密集型,那么只需要设置 CPU 核心数的线程就可以达到 CPU 利用率高,如果设置的太大,反而因为线程上下文切换影响性能,如果任务中有阻塞操作,而在阻塞的时间就可以让 CPU 去执行其他线程里的任务,我们可以通过 线程数量=内核数量 / (1 - 阻塞率)这个公式去计算合适的线程数,阻塞率我们可以通过计算任务总的执行时间和阻塞的时间获得。
目前微服务架构下有大量的RPC调用,所以利用多线程可以大大提高执行效率,我们可以借助分布式链路监控来统计RPC调用所消耗的时间,而这部分时间就是任务中阻塞的时间,当然为了做到的效率大,我们需要设置不同的值然后进行测试。

根据冯·诺依曼思想,计算机采用二进制作为数制基础,必须包含:运算器、控制器、存储设备,以及输入输出设备,如下图所示。
程序设计的5个底层逻辑,决定你能走多快
我们先来分析 CPU 的工作原理,现代 CPU 芯片中大都集成了,控制单元,运算单元,存储单元。控制单元是 CPU 的控制中心, CPU 需要通过它才知道下一步做什么,也就是执行什么指令,控制单元又包含:指令寄存器(IR ),指令译码器( ID )和操作控制器( OC )。
当程序被加载进内存后,指令就在内存中了,这个时候说的内存是独立于 CPU 外的主存设备,也就是 PC 机中的内存条,指令指针寄存器IP 指向内存中下一条待执行指令的地址,控制单元根据 IP寄存器的指向,将主存中的指令装载到指令寄存器。
这个指令寄存器也是一个存储设备,不过他集成在 CPU 内部,指令从主存到达 CPU 后只是一串 010101 的二进制串,还需要通过译码器解码,分析出操作码是什么,操作数在哪,之后就是具体的运算单元进行算术运算(加减乘除),逻辑运算(比较,位移)。而 CPU 指令执行过程大致为:取址(去主存获取指令放到寄存器),译码(从主存获取操作数放入高速缓存 L1 ),执行(运算)。
程序设计的5个底层逻辑,决定你能走多快
这里解释下上图中 CPU 内部集成的存储单元 SRAM ,正好和主存中的 DRAM 对应, RAM 是随机访问内存,就是给一个地址就能访问到数据,而磁盘这种存储媒介必须顺序访问,而 RAM 又分为动态和静态两种,静态 RAM 由于集成度较低,一般容量小,速度快,而动态 RAM 集成度较高,主要通过给电容充电和放电实现,速度没有静态 RAM 快,所以一般将动态 RAM 做为主存,而静态 RAM 作为 CPU 和主存之间的高速缓存 (cache),用来屏蔽 CPU 和主存速度上的差异,也就是我们经常看到的 L1 , L2 缓存。每一级别缓存速度变低,容量变大。
下图展示了存储器的层次化架构,以及 CPU 访问主存的过程,这里有两个知识点,一个是多级缓存之间为保证数据的一致性,而推出的缓存一致性协议,具体可以参考这篇文章,另外一个知识点是, cache 和主存的映射,首先要明确的是 cah 缓存的单位是缓存行,对应主存中的一个内存块,并不是一个变量,这个主要是因为 CPU 访问的空间局限性:被访问的某个存储单元,在一个较短时间内,很有可能再次被访问到,以及空间局限性:被访问的某个存储单元,在较短时间内,他的相邻存储单元也会被访问到。
而映射方式有很多种,类似于 cache 行号 = 主存块号 mod cache总行数 ,这样每次获取到一个主存地址,根据这个地址计算出在主存中的块号就可以计算出在 cache 中的行号。
程序设计的5个底层逻辑,决定你能走多快
下面我们接着聊 CPU 的指令执行。取址、译码、执行,这是一个指令的执行过程,所有指令都会严格按照这个顺序执行。但是多个指令之间其实是可以并行的,对于单核 CPU 来说,同一时刻只能有一条指令能够占有执行单元运行。这里说的执行是 CPU 指令处理 (取指,译码,执行) 三步骤中的第三步,也就是运算单元的计算任务。
所以为了提升 CPU 的指令处理速度,所以需要保证运算单元在执行前的准备工作都完成,这样运算单元就可以一直处于运算中,而刚刚的串行流程中,取指,解码的时候运算单元是空闲的,而且取指和解码如果没有命中高速缓存还需要从主存取,而主存的速度和 CPU 不在一个级别上,所以指令流水线 可以大大提高 CPU 的处理速度,下图是一个流水线的示例图,而现在的奔腾 CPU 都是32级流水线,具体做法就是将上面三个流程拆分的更细。
程序设计的5个底层逻辑,决定你能走多快
除了指令流水线, CPU 还有分支预测,乱序执行等优化速度的手段。好了,我们回到正题,一行 Java 代码是怎么执行的?
一行代码能够执行,必须要有可以执行的上下文环境,包括:指令寄存器、数据寄存器、栈空间等内存资源,然后这行代码必须作为一个执行流能够作系统的任务调度器识别,并给他分配 CPU 资源,当然这行代码所代表的指令必须是 CPU 可以解码识别的,所以一行 Java 代码必须被解释成对应的 CPU 指令才能执行。下面我们看下System.out.println("Hello world")这行代码的转译过程。
Java 是一门高级语言,这类语言不能直接运行在硬件上,必须运行在能够识别 Java 语言特性的虚拟机上,而 Java 代码必须通过 Java 编译器将其转换成虚拟机所能识别的指令序列,也称为 Java 字节码,之所以称为字节码是因为 Java 字节码的操作指令(OpCode)被固定为一个字节,以下为 System.out.println("Hello world") 编译后的字节码:
0x00: b2 00 02 getstatic Java .lang.System.out
0x03: 12 03 ldc "Hello, World!"
0x05: b6 00 04 invokevirtual Java .io.PrintStream.println
0x08: b1 return
左列是偏移;中间列是给虚拟机读的字节码;右列是高级语言的代码,下面是通过汇编语言转换成的机器指令,中间是机器码,第三列为对应的机器指令,后一列是对应的汇编代码:
0x00: 55 push rbp
0x01: 48 89 e5 mov rbp,rsp
0x04: 48 83 ec 10 sub rsp,0x10
0x08: 48 8d 3d 3b 00 00 00 lea rdi,[rip+0x3b]
; 加载 "Hello, World!\n"
0x0f: c7 45 fc 00 00 00 00 mov DWORD PTR [rbp-0x4],0x0
0x16: b0 00 mov al,0x0
0x18: e8 0d 00 00 00 call 0x12
; 调用 printf 方法
0x1d: 31 c9 xor ecx,ecx
0x1f: 89 45 f8 mov DWORD PTR [rbp-0x8],eax
0x22: 89 c8 mov eax,ecx
0x24: 48 83 c4 10 add rsp,0x10
0x28: 5d pop rbp
0x29: c3 ret
JVM 通过类加载器加载 class 文件里的字节码后,会通过解释器解释成汇编指令,终再转译成 CPU 可以识别的机器指令,解释器是软件来实现的,主要是为了实现同一份 Java 字节码可以在不同的硬件平台上运行,而将汇编指令转换成机器指令由硬件直接实现,这一步速度是很快的,当然 JVM 为了提高运行效率也可以将某些热点代码(一个方法内的代码)一次全部编译成机器指令后然后在执行,也就是和解释执行对应的即时编译(JIT), JVM 启动的时候可以通过 -Xint 和 -Xcomp 来控制执行模式。
从软件层面上, class 文件被加载进虚拟机后,类信息会存放在方法区,在实际运行的时候会执行方法区中的代码,在 JVM 中所有的线程共享堆内存和方法区,而每个线程有自己独立的 Java 方法栈,本地方法栈(面向 native 方法),PC寄存器(存放线程执行位置),当调用一个方法的时候, Java 虚拟机会在当前线程对应的方法栈中压入一个栈帧,用来存放 Java 字节码操作数以及局部变量,这个方法执行完会弹出栈帧,一个线程会连续执行多个方法,对应不同的栈帧的压入和弹出,压入栈帧后就是 JVM 解释执行的过程了。
程序设计的5个底层逻辑,决定你能走多快
中断
刚刚说到, CPU 只要一上电就像一个永动机, 不停的取指令,运算,周而复始,而中断便是操作系统的灵魂,故名思议,中断就是打断 CPU 的执行过程,转而去做点别的。
例如系统执行期间发生了致命错误,需要结束执行,例如用户程序调用了一个系统调用的方法,例如mmp等,就会通过中断让 CPU 切换上下文,转到内核空间,例如一个等待用户输入的程序正在阻塞,而当用户通过键盘完成输入,内核数据已经准备好后,就会发一个中断信号,唤醒用户程序把数据从内核取走,不然内核可能会数据溢出,当磁盘报了一个致命异常,也会通过中断通知 CPU ,定时器完成时钟滴答也会发时钟中断通知 CPU 。
中断的种类,我们这里就不做细分了,中断有点类似于我们经常说的驱动编程,而这个通知机制是怎么实现的呢,硬件中断的实现通过一个导线和 CPU 相连来传输中断信号,软件上会有特定的指令,例如执行系统调用创建线程的指令,而 CPU 每执行完一个指令,就会检查中断寄存器中是否有中断,如果有就取出然后执行该中断对应的处理程序。
陷入内核 : 我们在设计软件的时候,会考虑程序上下文切换的频率,频率太高肯定会影响程序执行性能,而陷入内核是针对 CPU 而言的, CPU 的执行从用户态转向内核态,以前是用户程序在使用 CPU ,现在是内核程序在使用 CPU ,这种切换是通过系统调用产生的。
系统调用是执行操作系统底层的程序,Linux的设计者,为了保护操作系统,将进程的执行状态用内核态和用户态分开,同一个进程中,内核和用户共享同一个地址空间,一般 4G 的虚拟地址,其中 1G 给内核态, 3G 给用户态。在程序设计的时候我们要尽量减少用户态到内核态的切换,例如创建线程是一个系统调用,所以我们有了线程池的实现。

计算机的外部设备有鼠标、键盘、打印机、网卡等,通常我们将外部设备和和主存之间的信息传递称为 I/O 操作 , 按操作特性可以分为,输出型设备,输入型设备,存储设备。现代设备都采用通道方式和主存进行交互,通道是一个专门用来处理IO任务的设备, CPU 在处理主程序时遇到I/O请求,启动通道上选址的设备,一旦启动成功,通道开始控制设备进行操作,而 CPU 可以继续执行其他任务,I/O 操作完成后,通道发出 I/O 操作结束的中断,处理器转而处理 IO 结束后的。其他处理 IO 的方式,例如轮询、中断、DMA,在性能上都不见通道,这里就不介绍了。当然 Java 程序和外部设备通信也是通过系统调用完成,这里也不在继续深入了。
重庆其他教育培训相关信息
2小时前
4月29日
注册时间:2020年04月16日
UID:686214
---------- 认证信息 ----------
手机已认证
查看用户主页