精华内容
下载资源
问答
  • 计算机可以从时间和空间两方面来区分指令和数据,时间上,取指周期从内存取出的是指令,而执行周期从内存取出或往内存写入的是数据,空间上,从内存取出指令送控制器,而执行周期从内存从取的数据送运算器...
    


    指令用来确定“做什么”和“怎样做”,数据是“做”的时候需要原始数。


    计算机可以从时间和空间两方面来区分指令和数据,在时间上,取指周期从内存中取出的是指令,而执行周期从内存取出或往内存中写入的是数据,在空间上,从内存中取出指令送控制器,而执行周期从内存从取的数据送运算器、往内存写入的数据也是来自于运算器。

    比如:要计算机做1+2=?中,“+”表示要做什么和怎样做,1和2则是做的时候需要的原始数。现在假设某CPU中,“+”用二进制“00000001”来表示,“1、2”分别用“00000001、00000010”来表示。那么,这段程序存入内存中就是这样的:
    XXXX1:00000001
    XXXX2:00000001
    XXXX3:00000010 前面的XXXX1 XXXX2
    XXXX3表示内存的地址
      从上面可以看出,“+”指令和被加数是完全相同的,当然,这是我故意这样假设的,但是,在实际情况中,这种情况是大量存在的。在正常情况下,CPU只能把XXXX1内存中的00000001作为指令,XXXX2内存中的00000001作为被加数才能得到正确的结果。那么CPU如何才能做到不把第二个00000001也当成“+”呢?
      1.人们把内存的某个地址规定为起始地址(又称为复位地址),也就是说,当计算机开机或者被强行复位(也就是机箱上那个重启动按钮按下的的时候),CPU立即跳转到这个地址中,并且把它里面的代码作为指令来执行,同时根据这个指令的长度和格式判断下一条指令在什么地方。
      对于X86系列CPU(也就是现在人们常用的什么奔XX、赛XX系列),它的复位地址是FFFF0,如果表示成逻辑地址则是:FFFF:0000。对DEBUG比较熟悉的朋友或者会在一些高级语言中嵌入汇编语言的朋友可以这样做一个试验:
      用DEBUG执行一条指令(这是一条无条件跳转指令):jmp
    FFFF:0000,或者在高级语言中嵌入这条汇编指令,执行后,你就会发现,计算机重新启动了。其实,用程序控制计算机重启的最本质的操作就是这样的。
      2.给各种指令规定了相应的长度和格式。比如:某数+某数这条指令就规定:这条指令的长度是3个字节,其中第一个字节表示“+”,后面两个字节表示被加数和加数。于是,当CPU到达这个指令后,就自动把第一个代码作为指令,后面两个代码作为数据,依次类推,第4个代码就必然是指令.....

    展开全文
  • 指令(伪操作)不像机器指令那样是程序运行期间由计算机来执行的,它是汇编程序对源程序汇编期间由汇编程序处理的操作.它可以完成如处理器选择,定义程序模式,定义数据,分配存储区,指示程序结束等功能.伪指令在...

    伪指令(伪操作)不像机器指令那样是在程序运行期间由计算机来执行的,它是在汇编程序对源程序汇编期间由汇编程序处理的操作.它可以完成如处理器选择,定义程序模式,定义数据,分配存储区,指示程序结束等功能.伪指令在编译的时候并不生成代码.伪指令在编译之后就不存在了。
    实际上就是假指令,不会产生机器代码,不会占用rom空间,只用于汇编过程中为汇编程序提供汇编信息,在汇编之后就消失了,是给汇编器来解释的。最常见的有:
    1、起始伪指令,标号ORG nn,定义程序或数据块的起始地址,指示此语句后面的程序或数据块以nn为起始地址连续存放在程序存储器中。
    2、字节定义伪指令,标号DB(字节常数或字符或表达式),指示在程序存储器中以标号为起始地址的单元里存放的数为字节数据(8位二进制数)。
    3、字定义伪指令,标号DW(字常数或表达式),指示在程序存储器中以标号为起始地址的单元里存放的数为字数据(16位的二进制数)。
    4、保留字节伪指令,标号DS (数值表达式),指示在程序存储器中保留以标号为起始地址的若干字节单元,其单元个数由数值表达式指定。
    5、等值伪指令,标号EQU(数值表达式),表示 EQU 两边的量等值,用于为标号或标识符赋值。
    6、位定义伪指令,标号BIT(位地址),同 EQU 指令,不过定义的是位操作地址。
    7、汇编结束伪指令,标号END,指示源程序段结束。 END 指令放在程序的最后。若将 END 放在程序中间,那么对于 END 后面的指令,汇编程序将不对其进行汇编。一个汇编语言源程序仅允许使用一个END 伪指令。

    如果想通过keil查看伪指令,可以通过伪指令的特性,即汇编之后不再存在,由此可以通过仿真模拟,再反汇编得到的机器指令,将反汇编得到的汇编代码与一开始的汇编代码进行比较即可,下面是反汇编得到的汇编代码:

    但是,这个时候是有问题的。如果进行仿真的话(运行),会出现死循环,这是由于我们导入了.A51文件,但是.A51文件与我们的代码出现了地址冲突(5个warning,其中有两个是无关的<截断>),所以,我们尽管可以得出结论(因为反汇编是没有问题的,即我们的程序是正确的,只是程序无法执行),但是我们的仿真是错误的,所以需要将.A51文件移除,再进行编译仿真!!!这个很重要,注意注意!!!
    去除之后得到的汇编指令主要是一条语句的参数发生了变化。这个时候我们就可以正常运行我们的程序,而不是像之前,会转到我们引入的.A51文件中而陷入死循环。!!!

    展开全文
  • CPU指令集是什么东西

    万次阅读 多人点赞 2017-02-16 09:37:09
    首先, 题主"李建国"自问自答的部分说的是正确的, CPU的指令集是软件与CPU这两个层级之间的接口, 而CPU自己, 就是对于这一套CPU指令集的"实例化". 无论处于上层的软件多么的高级, 想要CPU执行, 就必须被翻译成...

    (已更正) 这个问题包括CPU的硬件结构和汇编语言的范畴. 这里梳理一下.

    首先, 题主"李建国"自问自答的部分说的是正确的, CPU的指令集是软件与CPU这两个层级之间的接口, 而CPU自己, 就是对于这一套CPU指令集的"实例化".

    无论处于上层的软件多么的高级, 想要在CPU执行, 就必须被翻译成"机器码", 翻译这个工作由编译器来执行. 编译器在这个过程中, 要经过"编译", "汇编", "链接"几个步骤, 最后生成"可执行文件". 可执行文件中保存的是二进制机器码. 这串机器码可以直接被CPU读取和执行.

    软件意义上, "指令集"实际上是一个规范, 规范汇编的文件格式.
    以下为一条x86汇编代码:
    mov word ptr es:[eax + ecx * 8 + 0x11223344], 0x12345678

    这里可以体现出指令集的格式限制:
    1. 可以使用mov指令, 但它只能有2个操作数.
    2. 它的操作数长度是16 (word), 不要看到后面0x12345678就认为是32位操作数.
    3. 它带有段超越前缀, 这里使用了es, 还可以使用ds, cs, ss, fs, gs. 但是只能用这几个.
    4. 第一个操作数是一个内存地址, 第二个是立即数. 但是, 这个内存地址不能乱写, 写成[eax+ecx*10+0x11223344]就错了.

    实际上, 一条汇编指令与一段机器码是一一对应的. 上面这段汇, 可以被x86编译器翻译成几乎唯一的一段机器码:
    26 66 c7 84 c8 44 33 22 11 78 56
    上面提到的1,2,3,4点如果有一个弄错, 这一步就会失败.

    可以看出来, 指令集的作用, 就是告诉程序员/编译器, 汇编一定要有格式. 支持什么指令, 指令带什么限制条件, 用什么操作数, 用什么地址, 都是指令集规范的内容, 要是写错了, 就无法翻译成机器码.
    指令集规范汇编, 汇编可以翻译成机器码, 机器码告诉CPU每个周期去做什么. 因此, CPU指令集是描述CPU能实现什么功能的一个集合, 就是描述"CPU能使用哪些机器码"的集合".

    那机器码进入到CPU后又做什么呢?
    =====================编译器和CPU的分界线========================

    需要被执行的机器码先要被OS调度到内存之中, 程序执行时, 机器码依次经过了Memory--Cache--CPU fetch, 进入CPU流水线, 接着就要对它进行译码了, 译码工作生成的象是CPU内部数据格式, 微码(或者类似的格式, 这个格式不同的厂商会自己设计).

    这个过程画成图就是:

    软件层: 汇编语言
    ------------------------------------------------------------------------
    接口: 汇编语言所对应的机器码
    ------------------------------------------------------------------------
    硬件层: CPU使用内部数据结构进行运算

    如果机器码代表的功能是在指令集规范内的, 这条机器码就可以生产微码, 并在CPU内正常流动. 假设机器码是错误的, 是不可以通过CPU的译码阶段的, 控制电路一定会报错. 这种情况反映在Windows里往往都是蓝屏, 因为CPU无法继续执行, 它连下一条指令在哪都不知道.

    那么指令集在CPU里就代表: 只有CPU指令集范围内的指令可以被成功的译码, 并送往CPU流水线后端去执行.
    和常规的想法不一样, CPU不需要任何形式的存储介质去存储指令集, 因为"译码"这个步骤就是在对指令集里规范的机器码做解码. 硬件上, 译码这件事需要庞大数目的逻辑门阵列来实现.

    跳出格式这个圈子来看待这个问题. 可以说, CPU执行单元的能力, 决定了指令集的范围. 比如, CPU的执行单元有能力执行16位加法, 32位加法, 64位加法, 那么指令集里 一般就会有ADD 16, ADD 32, ADD 64这样的表达方式. 如果CPU的执行单元没有电路执行AVX指令, 那么指令集里 一般就没有VINSERTF128这样的指令供使用. 所以, 强有力的执行单元能够提供更多的指令集.

    再来看"CPU指令集在哪里"这个问题, 回答是, CPU本身就是CPU指令集. 指令集规定CPU可以做什么事, CPU就是具体做这件事的工具. 如果一定要指定一个狭义的CPU指令集的存放位置. 那就是CPU中的"译码电路".

    =======================================================================================================================
    =======================================================================================================================
    作者:Cascade
    链接:https://www.zhihu.com/question/20793038/answer/16198162
    来源:知乎
    著作权归作者所有,转载请联系作者获得授权。

    是,这个解释起来有点长。Be patient
    现代的CPU没拆过,我只在计算机组成原理实验课上用VHDL在某个实验平台上做过一个模拟的CPU。举个例子你可能比较好理解。
    比如我们设计一套指令集,其中肯定有条加法指令。比如Add R1 R2 。我们可以认为这条指令的意思是计算寄存器R1中的内容和R2的和,然后把结果存到R1寄存器中。
    那么经过编译后这条指令会变成二进制,比如010100010010 。这条二进制指令一共12位。明显可以分为三大部分。最前面的0101表示这是条加法指令,后面0001说的是第一个操作数是寄存器1,最后0010说的是第二个数就是寄存器2(其实实际没有这么简单的指令,至少应该区分操作数是寄存器还是直接的数据,但为了把这说的更容易理解作了简化)。我们可以通过十二根导线把这条指令输入一个CPU中。导线通电就是1,不通电就是0 。为了叙述方便我们从左到右用A0-A11给这12根导线编上号。
    然后计算机会分析这条指令。步骤如下:
    1. 最开始的两根导线A0和A1,第一根有电第二根没电,就能知道这是一条运算指令(而非存储器操作或者跳转等指令)。那么指令将被送入逻辑运算单元(ALU)去进行计算。其实很简单。只要这两根线控制接下来那部分电路开关即可。
    2. 接下来的A2和A3,01表示加法,那么就走加法运算那部分电路,关闭减法等运算电路。
    3. A4-A7将被送入寄存器电路,从中读取寄存器保存的值。送到ALU的第一个数据接口电路上。
    4. 后面的A8-A11同样被送入寄存器选择电路,接通R2寄存器,然后R2就把值送出来,放到ALU的第二个数据接口上。
    5. ALU开始运算,把两个接口电路上的数据加起来,然后输出。
    6. 最后结果又被送回R1。
    基本上简单的运算计算机就是这么操作的。他其实不知道你那些指令都是什么意思。具体的指令编程机器码后就会变成数字电路的开关信号。其中某几段会作为控制信号,控制其他部分的数据走不同的电路以执行运算。他没有一个地方保存着如何翻译这些机器码的字典,所有机器码的意义都被体现在整个电路的设计中了。
    当然,从汇编到机器码这步是汇编程序翻译的。汇编程序当然知道某条指令要翻译成什么样的机器码。

    ============================================================================================================================
    ============================================================================================================================



    展开全文
  • Java指令重排

    千次阅读 2019-11-28 10:57:20
    执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分3种类型: 编译器优化的重排序。编译器不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。 指令级并行的重排序。现代处理器...

    在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分3种类型:

    1. 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
    2. 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
    3. 内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

    1属于编译器重排序,2和3属于处理器重排序。从Java源代码到最终实际执行的指令序列,会分别经历下面3种重排序:
    Java中的指令重排.png

    as-if-serial 语义

    as-if-serial的意思是:不管指令怎么重排序,在单线程下执行结果不能被改变。不管是编译器级别还是处理器级别的重排序都必须遵循as-if-serial语义。

    为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序。但是as-if-serial规则允许对有控制依赖关系的指令做重排序,因为在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果,但是多线程下确有可能会改变结果。

    数据依赖

    int a = 1; // 1
    int b = 2; // 2
    int c = a + b; // 3
    

    上述代码,ab不存在依赖关系,所以1、2可以进行重排序;c依赖 ab,所以3必须在1、2的后面执行。

    控制依赖

    public void use(boolean flag, int a, int b) {
    	if (flag) { // 1
    		int i = a * b; // 2
    	}
    }
    

    flagi存在控制依赖关系。当指令重排序后,2这一步会将结果值写入重排序缓冲(Reorder Buffer,ROB)的硬件缓存中,当判断为true时,再把结果值写入变量i中。

    happens-before 语义

    JSR-133使用happens-before的概念来阐述操作之间的内存可见性。**在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。**这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。

    两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个 操作之前执行!happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一 个操作按顺序排在第二个操作之前(the first is visible to and ordered before the second)。

    happens-before 部分规则

    1. 程序顺序规则: 一个线程中的每个操作,happens-before于该线程中的任意后续操作。
      主要含义是:在一个线程内不管指令怎么重排序,程序运行的结果都不会发生改变。和as-if-serial 比较像。

    2. 监视器锁规则: 对一个锁的解锁,happens-before于随后对这个锁的加锁。
      主要含义是:同一个锁的解锁一定发生在加锁之后

    3. 管程锁定规则: 一个线程获取到锁后,它能看到前一个获取到锁的线程所有的操作结果。
      主要含义是:无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果!(管程是一种通用的同步原语,synchronized就是管程的实现)

    4. volatile变量规则: 对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
      主要含义是:如果一个线程先去写一个volatile变量,然后另一个线程又去读这个变量,那么这个写操作的结果一定对读的这个线程可见。

    5. 传递性: 如果A happens-before B,且B happens-before C,那么A happens-before C。

    6. start()规则: 如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
      主要含义是:线程A在启动子线程B之前对共享变量的修改结果对线程B可见。

    7. join()规则: 如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
      主要含义是:如果在线程A执行过程中调用了线程B的join方法,那么当B执行完成后,在线程B中所有操作结果对线程A可见。

    8. 线程中断规则: 对线程interrupt方法的调用happens-before于被中断线程的代码检测到中断事件的发生。
      主要含义是:响应中断一定发生在发起中断之后。

    9. 对象终结规则: 就是一个对象的初始化的完成,也就是构造函数执行的结束一定 happens-before它的finalize()方法。

    一个happens-before规则对应于一个或多个编译器和处理器重排序规则。

    as-if-serial和happens-before的主要作用都是:在保证不改变程序运行结果的前提下,允许部分指令的重排序,最大限度的提升程序执行的效率。

    内存屏障

    我们先来看一个并发环境下指令重排序带来的问题:
    并发环境下指令重排序带来的问题.png
    这里有两个线程A和线程B,当A执行init方法时发生了指令重排,2先执行,这时线程B执行use方法,这时我们拿到的变量a却还是0,所以最后得到的结果 i=0,而不是i=1。

    如何解决上述问题呢?一种是使用内存屏障(volatile),另一种使用临界区(synchronized )。

    如果我们使用内存屏障,那么JMM的处理器,会要求Java编译器在生成指令序列时,插入特定类型的内存屏障(Memory Barriers,Intel称之为 Memory Fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序。

    内存屏障的类型

    内存屏障的类型.png

    StoreLoad Barriers是一个“全能型”的屏障,它同时具有其他3个屏障的效果。现代的多处理器大多支持该屏障(其他类型的屏障不一定被所有处理器支持)。执行该屏障开销会很昂 贵,因为当前处理器通常要把写缓冲区中的数据全部刷新到内存中(Buffer Fully Flush)。

    常见处理器允许的重排序类型的列表,“N”表示处理器不允许两个操作重排序,“Y”表示允许重排序:

    常见处理器允许的重排序类型的列表.png

    那么上面的问题,我们可以在flag处插入一个内存屏障,其作用是:保证在init()方法中,第1步操作一定在第2步之前,禁止第1步和第2步操作出现指令重排序,代码如下:

    public class ControlDep {
    	int a = 0;
    	volatile boolean flag = false;
    
    	public void init() {
    		a = 1; // 1
    		flag = true; // 2
    		//.......
    	}
    
    	public void use() {
    		if (flag) { // 3
    			int i = a * a; // 4
    		}
    		//.......
    	}
    }
    

    A线程在写volatile变量之前所有可见的共享变量,在B线程读同一个volatile变量后,将立即变得对B线程可见。也就是说程序执行执行完第2步的时候,处理器会将第2步和其之前的所有结果强制刷新到主内存。也就是说a=1也会被强制刷新到主内存中。那么当另一个线程执行到步骤3的时候,如果判断到flag=true时,那么第4步处a一定是等于1的,这样就保证了程序的正确运行。

    顺序一致性

    顺序一致性内存模型是一个被计算机科学家理想化了的理论参考模型,它为程序员提供了极强的内存可见性保证。顺序一致性内存模型有两大特性:

    1. 一个线程中的所有操作必须按照程序的顺序来执行。
    2. (不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。

    JMM对正确同步的多线程程序的内存一致性做了如下保证:如果程序是正确同步的,程序的执行将具有顺序一致性(Sequentially Consistent)——即程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同。

    我们看到JMM仅仅是保证了程序运行的结果是和顺序执行是一致,并没有实现真正的顺一致性。它又是怎么实现的呢?JMM使用了临界区(加锁)来保证程序的顺序执行,但是在临界区内是允许出现指令重排的(JMM不允许临界区内的代码“逸出”到临界区之外,那样会破坏监视器的语义)。

    我们在回过来看下上面遇到的并发问题,在上面我们说了使用内存屏障来解决,这里我们使用临界区。

    public class ControlDep {
    	int a = 0;
    	boolean flag = false;
    
    	public synchronized void init() {
    		a = 1; // 1
    		flag = true; // 2
    		//.......
    	}
    
    	public synchronized void use() {
    		if (flag) { // 3
    			int i = a * a; // 4
    		}
    		//.......
    	}
    }
    

    虽然线程A执行init()方法时,在临界区内做了重排序,但由于监视器互斥执行的特性,线程B执行use()方法时,根本无法“观察”到线程A在临界区内的重排序。这种重排序既提高了执行效率,又没有改变程序的执行结果。
    两个内存模型中的执行时序对比图.jpg

    从这里我们可以看到,JMM在具体实现上的基本方针为:在不改变(正确同步的)程序执 行结果的前提下,尽可能地为编译器和处理器的优化打开方便之门。

    volatile的内存语义

    volatile的特性

    • 可见性: 对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入
    • 原子性: 对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性

    理解volatile特性的一个好方法是把对volatile变量的单个读/写,看成是使用同一个锁对这 些单个读/写操作做了同步。下面通过具体的示例来说明,示例代码如下:

    class VolatileFeaturesExample {
        // 使用volatile声明64位的long型变量
        volatile long vl = 0L;
    
        // 普通long型变量
        volatile long v2 = 0L;
    
        public void set(long l) {
            // 单个volatile变量的写
            vl = l;
        }
    
        public synchronized void syncSet(long l) {
            // 单个volatile变量的写执行效果等价于对普通变量的加同步锁来写
            v2 = l;
        }
    
        public long get() {
            // 单个volatile变量的读
            return vl;
        }
    
        public synchronized long syncGet() {
            // 单个volatile变量的读执行效果等价于对普通变量的加同步锁来读
            return vl;
        }
    
        public void getAndIncrement() {
            // 复合(多个)volatile变量的读/写 不具备原子性
            vl++;
            // v1++ 等价于 如下代码(不具备原子性)
            long temp = syncGet();
            temp = temp + 1;
            syncSet(temp);
        }
    }
    

    volatile写和读的内存语义

    • volatile写的内存语义: 当写一个volatile变量时,JMM会把该线程对应的本地内存中的所有共享变量值刷新到主内存
      volatile写的内存语义.png

    • volatile读的内存语义:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取所有共享变量。
      volatile读的内存语义.png

    volatile内存语义的实现

    为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

    具体限制规则如下

    volatile重排序规则表.jpg

    • 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保 volatile写之前的操作不会被编译器重排序到volatile写之后。
    • 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保 volatile读之后的操作不会被编译器重排序到volatile读之前。
    • 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。

    具体插入的内存屏障

    • 在每个volatile写操作的前面插入一个StoreStore屏障。在每个volatile写操作的后面插入一个StoreLoad屏障。
      volatile写操插入的内存屏障.png

    • 在每个volatile读操作的后面插入一个LoadLoad屏障。在每个volatile读操作的后面插入一个LoadStore屏障。
      volatile读操插入的内存屏障.png

    锁的内存语义

    • 当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。 线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A 对共享变量所做修改的)消息。
    • 当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共 享变量所做修改的)消息。

    线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息。

    final的内存语义

    1. 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。也就是说只有将对象实例化完成后,才能将对象引用赋值给变量。
    2. 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。也就是下面示例的4和5不能重排序。
    3. 当final域为引用类型时,在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

    下面通过代码在说明一下:

    public class FinalExample {
        int i;   // 普通变量
        final int j;   // final变量
        static FinalExample obj;
    
        public FinalExample() { // 构造函数
            i = 1;// 写普通域
            j = 2;// 写final域
        }
    
        public static void writer() {   // 写线程A执行
            // 这一步实际上有三个指令,如下:
            // memory = allocate();  // 1:分配对象的内存空间
            // ctorInstance(memory); // 2:初始化对象
            // instance = memory;  // 3:设置instance指向刚分配的内存地址
            obj = new FinalExample();
        }
    
        public static void reader() {   // 读线程B执行            
            FinalExample object = obj;  // 4. 读对象引用
            int a = object.i; // 5. 读普通域
            int b = object.j; // 读final域
        }
    }
    
    1. 如果没有final语义的保证,在writer()方法中,那三个指令可能发生重排序,导致步骤3先于2执行,然后线程B在执行reader()方法时拿到一个没有初始化的对象。
    2. 在读一个对象的final域之前,一定会先读包含这个final 域的对象的引用。在这个示例程序中,如果该引用不为null,那么引用对象的final域一定已经 被A线程初始化过了。

    final语义在处理器中的实现

    • 会要求编译器在final域的写之后,构造函数return之前插入一个StoreStore障屏。
    • 读final域的重排序规则要求编译器在读final域的操作前面插入一个LoadLoad屏障。

    源码

    https://github.com/wyh-spring-ecosystem-student/spring-boot-student/tree/releases

    spring-boot-student-concurrent 工程

    参考

    《java并发编程的艺术》

    layering-cache

    为监控而生的多级缓存框架 layering-cache这是我开源的一个多级缓存框架的实现,如果有兴趣可以看一下

    展开全文
  • 但有一个致命的问题,就是我的压栈操作写了子程序里,然而别忘了,ret指令会从栈顶弹出元素给IP,也就是下一条要执行的指令地址,但我却push了其他东西把原来保存的地址给压到下面去了,导致ret执行
  • 什么是AT指令

    千次阅读 2009-08-03 17:11:00
    AT指令介绍及用法 AT 指令AT 即Attention,AT指令集是从终端设备(Terminal Equipment,TE)或数据终端设备(Data Terminal Equipment,DTE)向终端适配器(Terminal Adapter, TA)或数据电路终端设备(Data ...
  • 指令系统——指令格式

    千次阅读 2021-05-14 15:08:15
    文章目录现代计算机的结构指令格式指令的定义指令格式零地址指令地址指令二、三地址指令地址指令地址码的位数有什么影响?分类指令-按地址码数目分类指令-按指令长度分类指令-按操作码长度分类指令—按操作类型...
  • JSP三大指令

    千次阅读 2016-11-11 10:24:08
    一.JSP指令 ...JSP指令共有三个:page、taglib、include。最常用的是page指令和taglib page指令 page指令是最为常用的指定,也是属性最多的属性! page指令没有必须属性,都是可选属性。例如<%@page
  • Java虚拟机常用指令

    千次阅读 2017-06-25 15:26:31
    const:用于特定的常量入栈,入栈的常量隐含在指令本身里。 比如:aconst_null将null压入操作数栈;iconst_m1将-1压入操作数栈; 指令助记符的第一个字符总是喜欢表示数据类型,i表示整数,l表示长整数,f表示浮点数...
  • 这个特定计算机,字是其用来一次性处理事务的一个固定长度的位(bit)组。一个字的位数(即字长)是计算机系统结构的一个重要特性。 2.CPU要读写一个内存单元时,必须给出这个内存单元的地址,内存地址由段...
  • 计算机组成与设计(指令

    千次阅读 2020-02-22 16:00:40
    指令集:一个给定的计算机体系结构所包含的指令集合,一种是人们编程书写的形式,另一种是计算机所能识别的形式。 共同目标:找到一种语言,可方便硬件和编译器的设计,且使性能最佳,同时使成本和功耗最低。 MIPS...
  • 之前阅读arm的汇编代码时,碰到了adr指令,查arm的指令手册,只说该指令是采用相对地址的,但这个相对地址应该怎么理解,却没有具体说明。之后网上以adr指令为关键字进行搜索,也没有找到进一步的知识。结果,...
  • 嵌入式 arm指令小结

    千次阅读 2014-04-20 17:12:30
    标准的ARM指令每条都是32位长,有些ARM核还可以执行Thmub指令集,该指令集是ARM指令集的子集,每条指令只有16位。 1 数据类型 ARM处理器一般支持下列6种数据类型: l8位有符号字节类型数据; l8位无符号字节...
  • 前面了解到了可以代码使用intrinsics函数来实现类似汇编的高级指令集(SSE等)指令这里,为了加深理解,再次分析一下SSE指令。 (2)MMX指令集 首先要提到MMX指令集,MMX指令集是SSE之前的,...
  • 微机原理判断指令是否正确。 1. MOV 1000H , BX ;指令错误,原因:立即数不能作为目标操作数 2. MOV BX , CL ;指令错误,原因:字长不一致 3. INC [BX] ;指令错误,原因:需指定操作数存储器操作字 4. MOV [BX...
  • ARM的常用指令

    千次阅读 2017-06-26 23:59:17
    一次逆向过程,我需要把bne改成beq,不知道我尚未掌握hopper的精髓还是什么,我始终无法像od直接修改指令,只能通过修memory write 改内存数据。转载的这篇博客,我知道了bne和beq十六进制的区别,从而...
  • Fetch(取指),也就是从 PC 寄存器里找到对应的指令地址,根据指令地址从内存里把具体的指令,加载到指令寄存器,然后把 PC 寄存器自增,好在未来执行下一条指令。 Decode(译码),也就是根据指令寄存器里面的...
  • 一、相同点 1、addr 和 offset 操作符都是获得操作数的偏移地址; 2、addr 和 offset 的处理都是先检查处理的是全局...(本来就是为了invoke指令中,使用局部变量的地址)  其他例如mov指令中,可以先使用l
  • 指令系统

    千次阅读 2015-04-24 10:33:51
    计算机有两种信息流,一种是数据流,它是处理的对象;一种是控制流,由它来对控制对数据信息的处理。 一、指令系统的发展   计算机的程序是由一系列的指令组成的,指令就是要计算机执行某种操作的命令。  ...
  • 这篇文章来自于农夫山泉,我们是大自然的搬运工,哈哈,文章来源于网络 ...首先, 题主"李建国"自问自答的部分说的是正确的,CPU的指令集是软件与CPU这两个层级之间的接口, 而CPU自己, 就是对于这一套CPU指令集的"...
  • 格式:“d 段寄存器:偏移地址”,以段寄存器的数据为段地址SA,列出从SA:偏移地址开始的内存区间的数据。以下是4个例子: ① -r ds :1000 -d ds:0 ;查看从1000:0开始的内存区间的内容 ② -r ds :1000...
  • ARM的---汇编指令

    万次阅读 多人点赞 2015-04-19 00:06:46
    带点的(一般都是ARM GNU伪汇编指令) 1. “.text”、“.data”、“.bss” 依次表示的是“以下是代码段”, “以下是初始化数据段”, “以下是未初始化数据段”。 2.".global" 定义一个全局符号,通常是为ld使用...
  • ARM指令

    千次阅读 2015-08-05 15:47:02
    Opcode是指令助记符,即操作码,说明指令需要执行的操作,在指令中是必需的。 (2)Cond项(command) Cond项表明了指令的执行的条件,每一条ARM指令都可以规定的条件下执行,每条ARM指令包含4位的条
  • JSP的三大指令

    千次阅读 2018-10-27 18:03:39
    1.JSP指令概述 2.page指令 1.page指令的pageEncoding和contentType(重点) 2.page指令的import属性  3.page指令的errorPage和isErrorPage  4.page指令的autFlush和buffer 5.page指令的isELIgnored 6.page...
  • thumb指令和arm指令

    2020-10-12 15:52:07
    许多复杂的功能复杂指令集(CISC)处理器上执行只需要单一的一条指令,即可,虽然可能有点慢,精简指令集处理器上可能需要多条指令,所以精简指令集(RISC)CPU上为了减少额外的指令对内存的消耗,考虑引入...
  • 0x00 - 吹牛逼其实这么说有些夸张,其实并不是没有条件的,标题那么取只不过是标题党罢了,吸波流量,骗点点赞关注什么的。 最近趁着iPhone SE2便宜,入了人生第一台苹果,最上头的那就是快捷指令了,这玩意可以编程...
  • 一、AT指令简介 待续 、AT指令使用实例 待续 三、注意问题 待续
  • ​费解了好久,一直不明白org是干嘛的。首先平时编程绝对不加org也能运行,为什么写引导... ​先说第个问题,BIOS自检等一系列工作完成后,要开始引导了。计算机会将硬盘0面0道1扇区512字节加载到07c00h(0000::7c

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 242,331
精华内容 96,932
关键字:

在二地址指令中什么是正确的