成都汇智动力老师直接接听

400-029-09** 400-029-0997 转 65635
查看完整号码
扫码拨号
微信扫码拨号

Java开发之高并发必备篇(三)——线程的内存模型

作者:汇智动力学院 来源:汇智动力学院 2022/10/6 14:20:37

上一篇文章中我们提到了Java代码运行的步骤中需要把变量从主...

上一篇文章中我们提到了Java代码运行的步骤中需要把变量从主内存中读取,计算修改完之后又需要写回主内存,那么这里面就设计到了JVM(java虚拟机)内存的结构。而想要深层次的了解并发和解决并发问题的影响并能够更充分的利用计算机处理器的效能,那么我们就需要对硬件内存架构、操作系统的线程模型以及JVM的内存进行了解才行,所以下面我们也是围绕这三个方面来讲解。 1.硬件内存架构 我们知道计算机中程序运行都需要依赖计算机中非常重要的一个硬件就是CPU(中央处理器)。早期CPU由 运算器 和 控制器 组成;控制器主要负责程序计数、指令寄存和译码等,计算器主要是算术逻辑单元运算和测试等。 但是CPU的运行计算速度太快了,而其他硬件比如IO操作(数据读取和写入)、网络、内存读取等等,跟cpu的速度比起来是差几个数量级的。如果不做处理,那么cpu和各个硬件之间的速度差异就非常明显,在处理程序的时候cpu很快就处理完并一直处于等待状态,这样就无法充分利用CPU的效能,浪费资源。 所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的 高速缓存 cache 部分来作为数据缓冲区,并且高速缓存也做了多级缓存用于充分的数据缓冲,例如现在的CPU大都是分L1一级缓存、L2二级缓存和L3三级缓存(运行速度L1,L2,L3依次降低),用于补偿CPU和内存、外围设备之间在操作速度上的差别。程序运行时将运算需要使用到的数据复制到缓存中,让运算能快速进行,当运算结束之后再从缓存同步回内存之中,这样处理器就无需等待缓慢的内存读写了。
这种设计在单核CPU中是没有问题的,但是如果是多核CPU就必须考虑另外一个问题了就是:缓存一致性(Cache Coherence),即如何解决在多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自缓存数据不一致的问题;为了解决这个问题就加入了一些协议用于保证 缓存的一致性 ,这类协议有MSI、MESI等。高速缓存交互设计如下:
不同结构的物理机器可以拥有不一样的内存模型,而JVM也有自己的内存模型。 除了增加高速缓存之外,为了使得处理器内部的运算单位能尽量被充分利用,处理器可能会对输入代码进行乱序执行优化,处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致(类似JVM的指令重排)。 2.操作系统中的多线程模型 之前提到过线程是CPU调度的最小单位,可以独立的完成任务。而操作系统中想要实现多线程,主要使用3种多线程模型: 内核线程模型 用户线程模型 混合线程模型 ①内核线程模型 Kernel-Level Thread 简称KLT,内核线程模型完全依赖于操作系统内核,内核保存线程的状态和信息,线程的创建、调度和管理由内核完成,并且系统内核负责将多个线程执行的任务映射到各个CPU中去执行。 用户的应用进程中的线程也是通过一对一的使用系统内核提供的轻量级进程LWP(Light weight process)接口来使用系统内核线程。 这种模型的好处是LWP在调用过程中即使阻塞了也不会影响整个进程的执行; 缺点是各种线程的操作都需要在用户态和内核态之间频繁切换,消耗太大,速度相对用户线程模型来说要慢。
②用户线程模型 User-Level Thread 简称ULT,用户线程模型不依赖操作系统核心,应用提供创建、同步、调度和管理线程函数来控制用户线程。不需要线程从用户态到内核态切换,速度快。 这种设计的好处是线程的各种操作以及切换消耗很低; 缺点是线程的所有操作都需要在用户态实现,线程的调度实现起来异常复杂,并且系统内核对ULT无感知,如果线程阻塞则会引起整个进程的阻塞。
③混合线程模型 混合线程模型是内核线程和用户线程的混合使用,用户线程仍然是在用户态中创建,用户线程的创建、切换和销毁的消耗很低,用户线程的数量不受限制。而LWP在用户线程和内核线程之间充当桥梁,就可以使用操作系统提供的线程调度和处理器映射功能。
我们 JVM虚拟机 中的线程模型就是基于操作系统提供的原生线程模型来实现的,而不同的操作系统导致JVM中线程的模型不同,比如Windows系统和Linux系统都是使用的内核线程模型,而Solaris系统支持用户内核混合线程模型和内核线程模型两种实现。 下面我们来验证下windows下java是内核线程还是用户线程:
在启动100个线程之前,查看下windows系统内核的线程数:
启动100个线程之后,windows系统内核线程数:
发现我们java线程和系统内核增加的线程数量几乎一致的,所以windows系统下java线程是内核线程的。 3.Java内存模型 java内存模型(Java Memory Model,简称JMM)是由JVM规范定义的,它实现了java程序在不同的硬件和操作系统平台上都能达到内存访问的一致性,而JMM中主要定义的是程序中变量的访问规则。 Java内存模型中,按照线程是否共享内存将虚拟机内存划分为两部分内存: 主内存 和 线 程工作内存 。
主内存:java虚拟机中规定所有变量都存储在主内存中,主内存是共享内存区域,所有线程都可以访问。上图中堆内存和方法区内存是主内存区域。 工作内存:java虚拟机中会为每个线程创建自己的工作内存,工作内存中存储了线程运行所需的主内存变量的副本数据,JVM中变量的运算和修改都需要在工作内存中进行,不能直接在主内存中进行,线程和线程之间也不能互相访问工作内存,工作内存运算完的结果写入主内存之后其他线程才能访问。上图中栈、本地方法栈和程序计数器就是工作内存区域了。 线程在主内存和工作内存之间交互如下图所示:

上图中也看到了,JVM中共定义了8种原子性(下面会讲解原子性)操作来实现主内存和工作内存的交互: read:将主内存中的一个变量的值读取出来 load:将read操作读取的变量值存储到工作内存的副本中 use::把工作内存中的变量的值传递给执行引擎 assign:把从执行引擎中接收的值赋值给工作内存中的变量 store:把工作内存中一个变量的值传递到主内存 write:将store操作传递的值写入到主内存的变量中 lock:将主内存中的一个变量标识为某个线程独占的锁定状态 unlock:将主内存中线程独占的一个变量从锁定状态中释放 通过上述8种原子操作的描述,我们如果要把一个变量从主内存传输到工作内存,那就要执行read和load操作,如果要把一个变量从工作内存写回主内存,就要执行store和write操作。 当然JVM的这几种操作我们是无法直接调用,我们只能通过java中指定好的一些关键字如synchronized等来调用部分操作。 线程操作的三个特性: Java内存模型中想要保证线程并发的安全,那么必须要了解原子性、可见性和有序性线程的这三个特性; 原子性(atomicity) 原子性操作在JVM中是线程安全的,非原子性操作在多线程并发下就会出现线程不安全的问题,这之后就需要我们使用同步技术把非原子性操作变成原子性操作。 JVM中lock和unlock就是把非原子性操作变成原子性操作,JVM中使用的两个字节码指令monitorenter和monitorexit来实现lock和unlock操作,而我们的java代码中则是使用synchronized关键字完成上述的两个操作。 可见性(Visibility) 简单来说就是在多线程操作变量的时候,一个线程修改了变量的值其它线程可以立即知晓这个修改。想要实现可见性需要线程在工作内存中修改了变量值之后立马同步到主内存中刷新主内存中变量的值,线程再次使用变量的时候重新从主内存中读取。这样其他线程在使用的会后就可以使用最新的变量值。 Java中线程的可见性可以通过三个关键字来实现,volatile、synchronized以及final。volatile关键字我们后面使用的会后再说,synchronized保证有序性是因为unlock操作之前必须把变量同步回主内存来实现的;final关键字是因为其修饰的变量在初始化后就会变成不会更改的常量,所以只要在初始化过程中没有把this引用传递出去被外部使用就能保证变量被线程调用的可见性。 有序性(Orderliness) 有序性是指在同一个线程中的所有操作都是有序执行的,但由于指令重排序等行为会导致指令执行的顺序不一定是按照代码中的先后顺序执行的。之前提到过在多线程操作中指令重排会导致线程不安全。在java中可以通过关键字volatile和synchronized保证线程的有序性。Volatile是因为变量被其修饰后指令就不会出现重排保证有序性,而synchronized是因为在变量被lock锁定之后同一时间只能被一个线程使用,即相当于单线程操作,而单线程的指令重排是没有问题的。 重谈指令重排和Happens-Before原则: 指令重排是编译器为了提高指令执行效率,只要程序的最终结果等同于它在严格的顺序化环境中执行的结果,就可以对指令的执行顺序进行重排序,也就是说重排序后指令的执行顺序不一定是代码的顺序。 在单线程下因指令重排的保证执行的结果所以前面指令执行的顺序是否按照代码的顺序不重要;但是在多线程下就会出现问题,例如一个线程A修改了一个变量且还没有来得及写入但是另外一个线程B却进行读取这个变量的时候就会出现问题,这个就是因为线程变量修改不可见和顺序改变引起的了。 上面讲到了volatile和synchronized关键字可以保可见性和有序性,但是Java内存模型中所有的可见性和有序性也不是都要依靠volatile和synchronized来实现,否则不仅会使得我们的一些操作变得非常繁琐,也会大大降低性能(synchronized滥用会对性能影响很大)。 JMM为保证线程操作的可见性和有序性定义了一个两个操作之间的偏序关系,称之为 Happens-Before(先行发生) 原则。比如说操作A先行发生于操作B,那么在B操作发生之前,A操作产生的变量修改等操作都会被操作B可见。 Happens-Before具体如下: 程序顺序规则 一个线程操作中,每个操作都先行发生于它的后续操作。简单来说就是按照代码逻辑发生。 监视器锁规则 一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须注意的是对同一个锁,后面是指时间上的后面。 volatile变量规则 对一个volatile变量的写操作先行发生与后面对这个变量的读操作。 线程启动规则 Thread对象的start()方法先行发生与该线程的每个动作。 线程终止规则 线程中的所有操作都先行发生与对此线程的终止检测,可以通过Thread.join()和Thread.isAlive()的返回值等手段检测线程是否已经终止执行。 线程中断规则 对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。 对象finalize规则 一个对象的初始化完成先行发生于他的 finalize 方法 (GC 回收对象调用的方法 ) 的执行。 传递性 如果操作A先行发生于操作B,操作B先行发生于操作C,那么操作A先行发生于操作C。 通过上述描述我们就可以知道我们衡量并发安全的问题需要以Happens-Before原则为准,而不是盲目的使用synchronized等关键字来实现并发的安全。 happens-Before中提到的线程的一些方法如:join()、isAlive()、interrupt()等方法,我们会在下篇文字中介绍“线程的操作方法”的时候带大家认识它们,这里就不做过多的解释了。 综上所述 从硬件内存到线程模型到我们JVM内存模型及其线程的各种特性和JMM先行发生规则,我们对于实现线程的并发安全就有了实现思路和理解,这也对我们后面理解实现线程安全的手段奠定了良好的基础。 二、算法的设计原则   ①、正确性:首先,算法应当满足以特定的“规则说明”方式给出的需求。其次,对算法是否“正确”的理解可以有以下四个层次:   一、程序语法错误。   二、程序对于几组输入数据能够得出满足需要的结果。   三、程序对于精心选择的、典型、苛刻切带有刁难性的几组输入数据能够得出满足要求的结果。   四、程序对于一切合法的输入数据都能得到满足要求的结果。   PS:通常以第 三 层意义的正确性作为衡量一个算法是否合格的标准。   ②、可读性:算法为了人的阅读与交流,其次才是计算机执行。因此算法应该易于人的理解;另一方面,晦涩难懂的程序易于隐藏较多的错误而难以调试。   ③、健壮性:当输入的数据非法时,算法应当恰当的做出反应或进行相应处理,而不是产生莫名其妙的输出结果。并且,处理出错的方法不应是中断程序执行,而是应当返回一个表示错误或错误性质的值,以便在更高的抽象层次上进行处理。   ④、高效率与低存储量需求:通常算法效率值得是算法执行时间;存储量是指算法执行过程中所需要的*大存储空间,两者都与问题的规模有关。   前面三点 正确性,可读性和健壮性相信都好理解。对于第四点算法的执行效率和存储量,我们知道比较算法的时候,可能会说“A算法比B算法快两倍”之类的话,但实际上这种说法没有任何意义。因为当数据项个数发生变化时,A算法和B算法的效率比例也会发生变化,比如数据项增加了50%,可能A算法比B算法快三倍,但是如果数据项减少了50%,可能A算法和B算法速度一样。所以描述算法的速度必须要和数据项的个数联系起来。也就是“大O”表示法,它是一种算法复杂度的相对表示方式,这里我简单介绍一下,后面会根据具体的算法来描述。   相对(relative):你只能比较相同的事物。你不能把一个做算数乘法的算法和排序整数列表的算法进行比较。但是,比较2个算法所做的算术操作(一个做乘法,一个做加法)将会告诉你一些有意义的东西;   表示(representation):大O(用它*简单的形式)把算法间的比较简化为了一个单一变量。这个变量的选择基于观察或假设。例如,排序算法之间的对比通常是基于比较操作(比较2个结点来决定这2个结点的相对顺序)。这里面就假设了比较操作的计算开销很大。但是,如果比较操作的计算开销不大,而交换操作的计算开销很大,又会怎么样呢?这就改变了先前的比较方式;   然后我们再说说算法的存储量,包括:    程序本身所占空间;    输入数据所占空间;    辅助变量所占空间;   一个算法的效率越高越好,而存储量是越低越好。 三、算法的分类 算法可以宏泛的分为三类: 一,有限的,确定性算法 这类算法在有限的一段时间内终止。他们可能要花很长时间来执行指定的任务,但仍将在一定的时间内终止。这类算法得出的结果常取决于输入值。 二,有限的,非确定算法 这类算法在有限的时间内终止。然而,对于一个(或一些)给定的数值,算法的结果并不是的或确定的。 三,无限的算法 是那些由于没有定义终止定义条件,或定义的条件无法由输入的数据满足而不终止运行的算法。通常,无限算法的产生是由于未能确定的定义终止条件。 Java中常见的算法有: ①、排序 排序就是对一组数据按照一定的顺序(从大到小或者从小到大)进行排序; 常见排序如下: 简单排序:冒泡排序、选择排序、插入排序; 高级排序:快速排序、希尔排序、归并排序、基数排序、鸡尾酒排序等等; ②、递归 递归是一种直接或者间接调用自身的一种算法,递归的目的是简化程序设计使程序更加易读; ③、查找 在一些(有序的/无序的)数据元素中,通过一定的方法找出与给定关键字相同的数据元素就叫做查找; ④、统计 指对有关数据的搜集、整理、计算、分析、解释、表述等的活动。 往期文章 墙裂推荐 [ 1 ] Java开发之高并发必备篇(二)——线程为什么会不安全? [2] Java开发之高并发必备篇(一)——线程基础 [3] Java开发篇——设计模式(5)装饰设计模式 原创视频 墙裂推荐 2022转行软件测试之校区资讯 ↓↓↓

活动福利 // 1 毕业礼包 | 毕业学员免费赠送《软件测试技术大咖专题课》,助力学员早日突破高薪瓶颈 // 2 入职礼包 |就业学员免费赠送《Java语言开发视频课》及全套源代码,市场价值12800元 // 3 推荐有奖 |推荐好友成功报名,立得丰厚“伯乐”红包 (欢迎详询校区老师)

详询软件测试&开发培训事宜

添加微信咨询
杨老师 @成都汇智动力

专业解答各类课程问题、介绍师资和学校情况

微信号:186******73

立即咨询

“成都汇智动力”是成都汇智动力信息技术有限公司在教育宝平台开设的店铺,若该店铺内信息涉嫌虚假或违法,请点击这里向教育宝反馈,我们将及时进行处理。

机构评分

环境:5.0师资:5.0服务:4.0效果:4.0

公示信息

店铺名称:成都汇智动力

单位名称:成都汇智动力信息技术有限公司

账号名称:cdhzdl(180******07)

所属城市:四川成都

入驻时长:12年

在线客服:在线聊

微信咨询

返回顶部