3.4 指令缓存

其实,不光处理器使用的数据被缓存,它们执行的指令也是被缓存的。只不过,指令缓存的问题相对来说要少得多,因为:

  • 执行的代码量取决于代码大小。而代码大小通常取决于问题复杂度。问题复杂度则是固定的。
  • 程序的数据处理逻辑是程序员设计的,而程序的指令却是编译器生成的。编译器的作者知道如何生成优良的代码。
  • 程序的流向比数据访问模式更容易预测。现如今的CPU很擅长模式检测,对预取很有利。
  • 代码永远都有良好的时间局部性和空间局部性。

有一些准则是需要程序员们遵守的,但大都是关于如何使用工具的,我们会在第6节介绍它们。而在这里我们只介绍一下指令缓存的技术细节。

随着CPU的核心频率大幅上升,缓存与核心的速度差越拉越大,CPU的处理开始管线化。也就是说,指令的执行分成若干阶段。首先,对指令进行解码,随后,准备参数,最后,执行它。这样的管线可以很长(例如,Intel的Netburst架构超过了20个阶段)。在管线很长的情况下,一旦发生延误(即指令流中断),需要很长时间才能恢复速度。管线延误发生在这样的情况下: 下一条指令未能正确预测,或者装载下一条指令耗时过长(例如,需要从内存读取时)。

为了解决这个问题,CPU的设计人员们在分支预测上投入大量时间和芯片资产(chip real estate),以降低管线延误的出现频率。

在CISC处理器上,指令的解码阶段也需要一些时间。x86及x86-64处理器尤为严重。近年来,这些处理器不再将指令的原始字节序列存入L1i,而是缓存解码后的版本。这样的L1i被叫做“追踪缓存(trace cache)”。追踪缓存可以在命中的情况下让处理器跳过管线最初的几个阶段,在管线发生延误时尤其有用。

前面说过,L2以上的缓存是统一缓存,既保存代码,也保存数据。显然,这里保存的代码是原始字节序列,而不是解码后的形式。

在提高性能方面,与指令缓存相关的只有很少的几条准则:

  1. 生成尽量少的代码。也有一些例外,如出于管线化的目的需要更多的代码,或使用小代码会带来过高的额外开销。
  2. 尽量帮助处理器作出良好的预取决策,可以通过代码布局或显式预取来实现。

这些准则一般会由编译器的代码生成阶段强制执行。至于程序员可以参与的部分,我们会在第6节介绍。

3.4.1 自修改的代码

在计算机的早期岁月里,内存十分昂贵。人们想尽千方百计,只为了尽量压缩程序容量,给数据多留一些空间。其中,有一种方法是修改程序自身,称为自修改代码(SMC)。现在,有时候我们还能看到它,一般是出于提高性能的目的,也有的是为了攻击安全漏洞。

一般情况下,应该避免SMC。虽然一般情况下没有问题,但有时会由于执行错误而出现性能问题。显然,发生改变的代码是无法放入追踪缓存(追踪缓存放的是解码后的指令)的。即使没有使用追踪缓存(代码还没被执行或有段时间没执行),处理器也可能会遇到问题。如果某个进入管线的指令发生了变化,处理器只能扔掉目前的成果,重新开始。在某些情况下,甚至需要丢弃处理器的大部分状态。

最后,由于处理器认为代码页是不可修改的(这是出于简单化的考虑,而且在99.9999999%情况下确实是正确的),L1i用到并不是MESI协议,而是一种简化后的SI协议。这样一来,如果万一检测到修改的情况,就需要作出大量悲观的假设。

因此,对于SMC,强烈建议能不用就不用。现在内存已经不再是一种那么稀缺的资源了。最好是写多个函数,而不要根据需要把一个函数改来改去。也许有一天可以把SMC变成可选项,我们就能通过这种方式检测入侵代码。如果一定要用SMC,应该让写操作越过缓存,以免由于L1i需要L1d里的数据而产生问题。更多细节,请参见6.1节。

在Linux上,判断程序是否包含SMC是很容易的。利用正常工具链(toolchain)构建的程序代码都是写保护(write-protected)的。程序员需要在链接时施展某些关键的魔术才能生成可写的代码页。现代的Intel x86和x86-64处理器都有统计SMC使用情况的专用计数器。通过这些计数器,我们可以很容易判断程序是否包含SMC,即使它被准许运行。

3.5 缓存未命中的因素

我们已经看过内存访问没有命中缓存时,那陡然猛涨的高昂代价。但是有时候,这种情况又是无法避免的,因此我们需要对真正的代价有所认识,并学习如何缓解这种局面。

3.5.1 缓存与内存带宽

为了更好地理解处理器的能力,我们测量了各种理想环境下能够达到的带宽值。由于不同处理器的版本差别很大,所以这个测试比较有趣,也因为如此,这一节都快被测试数据灌满了。我们使用了x86和x86-64处理器的SSE指令来装载和存储数据,每次16字节。工作集则与其它测试一样,从1kB增加到512MB,测量的具体对象是每个周期所处理的字节数。

图3.24展示了一颗64位Intel Netburst处理器的性能图表。当工作集能够完全放入L1d时,处理器的每个周期可以读取完整的16字节数据,即每个周期执行一条装载指令(moveaps指令,每次移动16字节的数据)。测试程序并不对数据进行任何处理,只是测试读取指令本身。当工作集增大,无法再完全放入L1d时,性能开始急剧下降,跌至每周期6字节。在218工作集处出现的台阶是由于DTLB缓存耗尽,因此需要对每个新页施加额外处理。由于这里的读取是按顺序的,预取机制可以完美地工作,而FSB能以5.3字节/周期的速度传输内容。但预取的数据并不进入L1d。当然,真实世界的程序永远无法达到以上的数字,但我们可以将它们看作一系列实际上的极限值。

更令人惊讶的是写操作和复制操作的性能。即使是在很小的工作集下,写操作也始终无法达到4字节/周期的速度。这意味着,Intel为Netburst处理器的L1d选择了写通(write-through)模式,所以写入性能受到L2速度的限制。同时,这也意味着,复制测试的性能不会比写入测试差太多(复制测试是将某块内存的数据拷贝到另一块不重叠的内存区),因为读操作很快,可以与写操作实现部分重叠。最值得关注的地方是,两个操作在工作集无法完全放入L2后出现了严重的性能滑坡,降到了0.5字节/周期!比读操作慢了10倍!显然,如果要提高程序性能,优化这两个操作更为重要。

再来看图3.25,它来自同一颗处理器,只是运行双线程,每个线程分别运行在处理器的一个超线程上。

图3.25采用了与图3.24相同的刻度,以方便比较两者的差异。图3.25中的曲线抖动更多,是由于采用双线程的缘故。结果正如我们预期,由于超线程共享着几乎所有资源(仅除寄存器外),所以每个超线程只能得到一半的缓存和带宽。所以,即使每个线程都要花上许多时间等待内存,从而把执行时间让给另一个线程,也是无济于事——因为另一个线程也同样需要等待。这里恰恰展示了使用超线程时可能出现的最坏情况。

再来看Core 2处理器的情况。看看图3.26和图3.27,再对比下P4的图3.24和3.25,可以看出不小的差异。Core 2是一颗双核处理器,有着共享的L2,容量是P4 L2的4倍。但更大的L2只能解释写操作的性能下降出现较晚的现象。

当然还有更大的不同。可以看到,读操作的性能在整个工作集范围内一直稳定在16字节/周期左右,在220处的下降同样是由于DTLB的耗尽引起。能够达到这么高的数字,不但表明处理器能够预取数据,并且按时完成传输,而且还意味着,预取的数据是被装入L1d的。

写/复制操作的性能与P4相比,也有很大差异。处理器没有采用写通策略,写入的数据留在L1d中,只在必要时才逐出。这使得写操作的速度可以逼近16字节/周期。一旦工作集超过L1d,性能即飞速下降。由于Core 2读操作的性能非常好,所以两者的差值显得特别大。当工作集超过L2时,两者的差值甚至超过20倍!但这并不表示Core 2的性能不好,相反,Core 2永远都比Netburst强。

在图3.27中,启动双线程,各自运行在Core 2的一个核心上。它们访问相同的内存,但不需要完美同步。从结果上看,读操作的性能与单线程并无区别,只是多了一些多线程情况下常见的抖动。

有趣的地方来了——当工作集小于L1d时,写操作与复制操作的性能很差,就好像数据需要从内存读取一样。两个线程彼此竞争着同一个内存位置,于是不得不频频发送RFO消息。问题的根源在于,虽然两个核心共享着L2,但无法以L2的速度处理RFO请求。而当工作集超过L1d后,性能出现了迅猛提升。这是因为,由于L1d容量不足,于是将被修改的条目刷新到共享的L2。由于L1d的未命中可以由L2满足,只有那些尚未刷新的数据才需要RFO,所以出现了这样的现象。这也是这些工作集情况下速度下降一半的原因。这种渐进式的行为也与我们期待的一致: 由于每个核心共享着同一条FSB,每个核心只能得到一半的FSB带宽,因此对于较大的工作集来说,每个线程的性能大致相当于单线程时的一半。

由于同一个厂商的不同处理器之间都存在着巨大差异,我们没有理由不去研究一下其它厂商处理器的性能。图3.28展示了AMD家族10h Opteron处理器的性能。这颗处理器有64kB的L1d、512kB的L2和2MB的L3,其中L3缓存由所有核心所共享。

大家首先应该会注意到,在L1d缓存足够的情况下,这个处理器每个周期能处理两条指令。读操作的性能超过了32字节/周期,写操作也达到了18.7字节/周期。但是,不久,读操作的曲线就急速下降,跌到2.3字节/周期,非常差。处理器在这个测试中并没有预取数据,或者说,没有有效地预取数据。

另一方面,写操作的曲线随几级缓存的容量而流转。在L1d阶段达到最高性能,随后在L2阶段下降到6字节/周期,在L3阶段进一步下降到2.8字节/周期,最后,在工作集超过L3后,降到0.5字节/周期。它在L1d阶段超过了Core 2,在L2阶段基本相当(Core 2的L2更大一些),在L3及主存阶段比Core 2慢。

复制的性能既无法超越读操作的性能,也无法超越写操作的性能。因此,它的曲线先是被读性能压制,随后又被写性能压制。

图3.29显示的是Opteron处理器在多线程时的性能表现。

读操作的性能没有受到很大的影响。每个线程的L1d和L2表现与单线程下相仿,L3的预取也依然表现不佳。两个线程并没有过渡争抢L3。问题比较大的是写操作的性能。两个线程共享的所有数据都需要经过L3,而这种共享看起来却效率很差。即使是在L3足够容纳整个工作集的情况下,所需要的开销仍然远高于L3的访问时间。再来看图3.27,可以发现,在一定的工作集范围内,Core 2处理器能以共享的L2缓存的速度进行处理。而Opteron处理器只能在很小的一个范围内实现相似的性能,而且,它仅仅只能达到L3的速度,无法与Core 2的L2相比。

3.5.2 关键字加载

内存以比缓存线还小的块从主存储器向缓存传送。如今64位可一次性传送,缓存线的大小为64或128比特。这意味着每个缓存线需要8或16次传送。

DRAM芯片可以以触发模式传送这些64位的块。这使得不需要内存控制器的进一步指令和可能伴随的延迟,就可以将缓存线充满。如果处理器预取了缓存,这有可能是最好的操作方式。

如果程序在访问数据或指令缓存时没有命中(这可能是强制性未命中或容量性未命中,前者是由于数据第一次被使用,后者是由于容量限制而将缓存线逐出),情况就不一样了。程序需要的并不总是缓存线中的第一个字,而数据块的到达是有先后顺序的,即使是在突发模式和双倍传输率下,也会有明显的时间差,一半在4个CPU周期以上。举例来说,如果程序需要缓存线中的第8个字,那么在首字抵达后它还需要额外等待30个周期以上。

当然,这样的等待并不是必需的。事实上,内存控制器可以按不同顺序去请求缓存线中的字。当处理器告诉它,程序需要缓存中具体某个字,即「关键字(critical word)」时,内存控制器就会先请求这个字。一旦请求的字抵达,虽然缓存线的剩余部分还在传输中,缓存的状态还没有达成一致,但程序已经可以继续运行。这种技术叫做关键字优先及较早重启(Critical Word First & Early Restart)。

现在的处理器都已经实现了这一技术,但有时无法运用。比如,预取操作的时候,并不知道哪个是关键字。如果在预取的中途请求某条缓存线,处理器只能等待,并不能更改请求的顺序。

在关键字优先技术生效的情况下,关键字的位置也会影响结果。图3.30展示了下一个测试的结果,图中表示的是关键字分别在线首和线尾时的性能对比情况。元素大小为64字节,等于缓存线的长度。图中的噪声比较多,但仍然可以看出,当工作集超过L2后,关键字处于线尾情况下的性能要比线首情况下低0.7%左右。而顺序访问时受到的影响更大一些。这与我们前面提到的预取下条线时可能遇到的问题是相符的。

3.5.3 缓存设定

缓存放置的位置与超线程,内核和处理器之间的关系,不在程序员的控制范围之内。但是程序员可以决定线程执行的位置,接着高速缓存与使用的CPU的关系将变得非常重要。

这里我们将不会深入(探讨)什么时候选择什么样的内核以运行线程的细节。我们仅仅描述了在设置关联线程的时候,程序员需要考虑的系统结构的细节。

超线程,通过定义,共享除去寄存器集以外的所有数据。包括 L1 缓存。这里没有什么可以多说的。多核处理器的独立核心带来了一些乐趣。每个核心都至少拥有自己的 L1 缓存。除此之外,下面列出了一些不同的特性:

  • 早期多核心处理器有独立的 L2 缓存且没有更高层级的缓存。
  • 之后英特尔的双核心处理器模型拥有共享的L2 缓存。对四核处理器,则分对拥有独立的L2 缓存,且没有更高层级的缓存。
  • AMD 家族的 10h 处理器有独立的 L2 缓存以及一个统一的L3 缓存。

关于各种处理器模型的优点,已经在它们各自的宣传手册里写得够多了。在每个核心的工作集互不重叠的情况下,独立的L2拥有一定的优势,单线程的程序可以表现优良。考虑到目前实际环境中仍然存在大量类似的情况,这种方法的表现并不会太差。不过,不管怎样,我们总会遇到工作集重叠的情况。如果每个缓存都保存着某些通用运行库的常用部分,那么很显然是一种浪费。

如果像Intel的双核处理器那样,共享除L1外的所有缓存,则会有一个很大的优点。如果两个核心的工作集重叠的部分较多,那么综合起来的可用缓存容量会变大,从而允许容纳更大的工作集而不导致性能的下降。如果两者的工作集并不重叠,那么则是由Intel的高级智能缓存管理(Advanced Smart Cache management)发挥功用,防止其中一个核心垄断整个缓存。

即使每个核心只使用一半的缓存,也会有一些摩擦。缓存需要不断衡量每个核心的用量,在进行逐出操作时可能会作出一些比较差的决定。我们来看另一个测试程序的结果。

这次,测试程序两个进程,第一个进程不断用SSE指令读/写2MB的内存数据块,选择2MB,是因为它正好是Core 2处理器L2缓存的一半,第二个进程则是读/写大小变化的内存区域,我们把这两个进程分别固定在处理器的两个核心上。图中显示的是每个周期读/写的字节数,共有4条曲线,分别表示不同的读写搭配情况。例如,标记为读/写(read/write)的曲线代表的是后台进程进行写操作(固定2MB工作集),而被测量进程进行读操作(工作集从小到大)。

图中最有趣的是220到223之间的部分。如果两个核心的L2是完全独立的,那么所有4种情况下的性能下降均应发生在221到222之间,也就是L2缓存耗尽的时候。但从图上来看,实际情况并不是这样,特别是背景进程进行写操作时尤为明显。当工作集达到1MB(220)时,性能即出现恶化,两个进程并没有共享内存,因此并不会产生RFO消息。所以,完全是缓存逐出操作引起的问题。目前这种智能的缓存处理机制有一个问题,每个核心能实际用到的缓存更接近1MB,而不是理论上的2MB。如果未来的处理器仍然保留这种多核共享缓存模式的话,我们唯有希望厂商会把这个问题解决掉。

推出拥有双L2缓存的4核处理器仅仅只是一种临时措施,是开发更高级缓存之前的替代方案。与独立插槽及双核处理器相比,这种设计并没有带来多少性能提升。两个核心是通过同一条总线(被外界看作FSB)进行通信,并没有什么特别快的数据交换通道。

未来,针对多核处理器的缓存将会包含更多层次。AMD的10h家族是一个开始,至于会不会有更低级共享缓存的出现,还需要我们拭目以待。我们有必要引入更多级别的缓存,因为频繁使用的高速缓存不可能被许多核心共用,否则会对性能造成很大的影响。我们也需要更大的高关联性缓存,它们的数量、容量和关联性都应该随着共享核心数的增长而增长。巨大的L3和适度的L2应该是一种比较合理的选择。L3虽然速度较慢,但也较少使用。

对于程序员来说,不同的缓存设计就意味着调度决策时的复杂性。为了达到最高的性能,我们必须掌握工作负载的情况,必须了解机器架构的细节。好在我们在判断机器架构时还是有一些支援力量的,我们会在后面的章节介绍这些接口。

3.5.4 FSB的影响

FSB在性能中扮演了核心角色。缓存数据的存取速度受制于内存通道的速度。我们做一个测试,在两台机器上分别跑同一个程序,这两台机器除了内存模块的速度有所差异,其它完全相同。图3.32展示了Addnext0测试(将下一个元素的pad[0]加到当前元素的pad[0]上)在这两台机器上的结果(NPAD=7,64位机器)。两台机器都采用Core 2处理器,一台使用667MHz的DDR2内存,另一台使用800MHz的DDR2内存(比前一台增长20%)。

图上的数字表明,当工作集大到对FSB造成压力的程度时,高速FSB确实会带来巨大的优势。在我们的测试中,性能的提升达到了18.5%,接近理论上的极限。而当工作集比较小,可以完全纳入缓存时,FSB的作用并不大。当然,这里我们只测试了一个程序的情况,在实际环境中,系统往往运行多个进程,工作集是很容易超过缓存容量的。

如今,一些英特尔的处理器,支持前端总线(FSB)的速度高达1,333 MHz,这意味着速度有另外60%的提升。将来还会出现更高的速度。速度是很重要的,工作集会更大,快速的RAM和高FSB速度的内存肯定是值得投资的。我们必须小心使用它,因为即使处理器可以支持更高的前端总线速度,但是主板的北桥芯片可能不会。使用时,检查它的规范是至关重要的。

------------------------------------------------------------------------------------------------------------------------------------------

对于jvm 层面:

所有线程共享主内存
每个线程有自己的工作内存

refreshing local memory to/from main memory must  comply to JMM rules

产生线程安全的原因

线程的working memory是cpu的寄存器和高速缓存的抽象描述:现在的计算机,cpu在计算的时候,并不总是从内存读取数据,它的数据读取顺序优先级 是:寄存器-高速缓存-内存。线程耗费的是CPU,线程计算的时候,原始的数据来自内存,在计算过程中,有些数据可能被频繁读取,这些数据被存储在寄存器和高速缓存中,当线程计算完后,这些缓存的数据在适当的时候应该写回内存。当多个线程同时读写某个内存数据时,就会产生多线程并发问题,涉及到三个特 性:原子性,有序性,可见性。 支持多线程的平台都会面临 这种问题,运行在多线程平台上支持多线程的语言应该提供解决该问题的方案。

JVM是一个虚拟的计算机,它也会面临多线程并发问题,java程序运行在java虚拟机平台上,java程序员不可能直接去控制底层线程对寄存器高速缓存内存之间的同步,那么java从语法层面,应该给开发人员提供一种解决方案,这个方案就是诸如 synchronized, volatile,锁机制(如同步块,就绪队 列,阻塞队列)等等。这些方案只是语法层面的,但我们要从本质上去理解它;

每个线程都有自己的执行空间(即工作内存),线程执行的时候用到某变量,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作:读取,修改,赋值等,这些均在工作内存完成,操作完成后再将变量写回主内存;

各个线程都从主内存中获取数据,线程之间数据是不可见的;打个比方:主内存变量A原始值为1,线程1从主内存取出变量A,修改A的值为2,在线程1未将变量A写回主内存的时候,线程2拿到变量A的值仍然为1。

这便引出“可见性”的概念:当一个共享变量在多个线程的工作内存中都有副本时,如果一个线程修改了这个共享变量的副本值,那么其他线程应该能够看到这个被修改后的值,这就是多线程的可见性问题。

普通变量情况:如线程A修改了一个普通变量的值,然后向主内存进行写回,另外一条线程B在线程A回写完成了之后再从主内存进行读取操作,新变量的值才会对线程B可见。

如何保证线程安全 
编写线程安全的代码,本质上就是管理对状态(state)的访问,而且通常都是共享的、可变的状态。这里的状态就是对象的变量(静态变量和实例变量) 
线程安全的前提是该变量是否被多个线程访问, 保证对象的线程安全性需要使用同步来协调对其可变状态的访问;若是做不到这一点,就会导致脏数据和其他不可预期的后果。无论何时,只要有多于一个的线程访问给定的状态变量,而且其中某个线程会写入该变量,此时必须使用同步来协调线程对该变量的访问。Java中首要的同步机制是synchronized关键字,它提供了独占锁。除此之外,术语“同步”还包括volatile变量,显示锁和原子变量的使用。 
在没有正确同步的情况下,如果多个线程访问了同一个变量,你的程序就存在隐患。有3种方法修复它: 
(1)不要跨线程共享变量
(2)使状态变量为不可变的
(3)或者 在任何访问状态变量的时候使用同步

volatile要求程序对变量的每次修改,都写回主内存,这样便对其它线程课件,解决了可见性的问题,但是不能保证数据的一致性;特别注意:原子操作:根据Java规范,对于基本类型的赋值或者返回值操作,是原子操作。但这里的基本数据类型不包括long和double, 因为JVM看到的基本存储单位是32位,而long 和double都要用64位来表示。所以无法在一个时钟周期内完成

通俗的讲一个对象的状态就是它的数据,存储在状态变量中,比如实例域或者静态域;无论何时,只要多于一个的线程访问给定的状态变量。而且其中某个线程会写入该变量,此时必须使用同步来协调线程对该变量的访问。

同步锁:每个JAVA对象都有且只有一个同步锁,在任何时刻,最多只允许一个线程拥有这把锁。

当一个线程试图访问带有synchronized(this)标记的代码块时,必须获得 this关键字引用的对象的锁,在以下的两种情况下,本线程有着不同的命运。
1、 假如这个锁已经被其它的线程占用,JVM就会把这个线程放到本对象的锁池中。本线程进入阻塞状态。锁池中可能有很多的线程,等到其他的线程释放了锁,JVM就会从锁池中随机取出一个线程,使这个线程拥有锁,并且转到就绪状态。
2、 假如这个锁没有被其他线程占用,本线程会获得这把锁,开始执行同步代码块。
(一般情况下在执行同步代码块时不会释放同步锁,但也有特殊情况会释放对象锁
如在执行同步代码块时,遇到异常而导致线程终止,锁会被释放;在执行代码块时,执行了锁所属对象的wait()方法,这个线程会释放对象锁,进入对象的等待池中)

Synchronized关键字保证了数据读写一致和可见性等问题,但是他是一种阻塞的线程控制方法,在关键字使用期间,所有其他线程不能使用此变量,这就引出了一种叫做非阻塞同步的控制线程安全的需求。

产生线程安全的原因(4)(操作系统)相关推荐

  1. 多线程编程:线程死锁的原因以及解决方法

    多线程编程:线程死锁的原因以及解决方法 关于线程死锁这个问题相信程序员在编写多线程程序时会经常遇到的一个经典问题,这种情况往往出现在多个线程同时对临界资源访问时所产生的. 属于临界资源的硬件有打印机. ...

  2. 调试JDK源码-Hashtable实现原理以及线程安全的原因

    调试JDK源码-一步一步看HashMap怎么Hash和扩容 调试JDK源码-ConcurrentHashMap实现原理 调试JDK源码-HashSet实现原理 调试JDK源码-调试JDK源码-Hash ...

  3. java 线程安全的原因_Java并发编程——线程安全性深层原因

    线程安全性深层原因 这里我们将会从计算机硬件和编辑器等方面来详细了解线程安全产生的深层原因. 缓存一致性问题 CPU内存架构 随着CPU的发展,而因为CPU的速度和内存速度不匹配的问题(CPU寄存器的 ...

  4. 线程安全问题的原因和解决方案

    一.首先产生线程安全的原因: (一)站在开发者的角度看:①多个线程之间操作同一个数据②至少有一个线程修改这个数据(不是读操作而是写操作) (二)站在系统的角度看:一条语句对应多个指令,线程调度可以发生 ...

  5. 导致线程死锁的原因?怎么解除线程死锁

    欢迎大家关注我的公众号[老周聊架构],Java后端主流技术栈的原理.源码分析.架构以及各种互联网高并发.高性能.高可用的解决方案. 一.导致线程死锁的原因 多个线程同时被阻塞,它们中的一个或者全部都在 ...

  6. 引发线程安全的原因是什么?怎么解决?程序员一定要掌握的东西

    目录 前言 一.引发线程安全的原因 1.抢占式执行 2.多线程修改同一个变量 3.操作是原子的 4.指令重排序 5.内存的可见性问题 对于线程不安全问题,如何解决? Java的代码中如何进行加锁呢? ...

  7. 产生线程安全的原因(3)(操作系统)

    3.3.3 写入时的行为 在我们开始研究多个线程或进程同时使用相同内存之前,先来看一下缓存实现的一些细节.我们要求缓存是一致的,而且这种一致性必须对用户级代码完全透明.而内核代码则有所不同,它有时候需 ...

  8. 产生线程安全的原因(1)(操作系统)

    首先了解cpu 告诉缓存问题和架构 这是Ulrich Drepper写"程序员都该知道存储器"的第二部.那些没有读过第一部 的读者可能希望从这一部开始.这本书写的非常好,并且感谢U ...

  9. 产生线程安全的原因(2)(操作系统)

    3.3.2 Cache的性能测试 用于测试程序的数据可以模拟一个任意大小的工作集:包括读.写访问,随机.连续访问.在图3.4中我们可以看到,程序为工作集创建了一个与其大小和元素类型相同的数组: str ...

最新文章

  1. 有关 ecshop 属性 {$goods.goods_attr|nl2br} 标签的赋值问题
  2. vs code搭建Django环境
  3. 【Flask】CORS同源问题解决
  4. linux ssh Unused,ssh免密码登录 - leopardlinux的个人空间 - OSCHINA - 中文开源技术交流社区...
  5. 组合模式——透明组合模式,安全组合模式
  6. Containerd迎来1.0通用版本
  7. python输入print跳到documentation-习题 48: 更复杂的用户输入
  8. 【云栖直播】精彩推荐第3期:个性化推荐系统搭建实践
  9. Docker的应用场景
  10. android和网络连接相关的类URL,URLConnection,HttpURLConnection,HttpClient
  11. HTML5的音频播放和视频播放
  12. 【macOS付费软件推荐】第6期:Reeder
  13. js截取视频第一帧_JS截取视频第一帧
  14. 米家摄像头显示服务器错误,米家摄像头云服务器
  15. 【java】我国最高山峰是珠穆朗玛峰,8848米。现在我有一张足够大的纸,它的厚度是0.01米。请问,我折叠多少次,可以折成珠穆朗玛峰的高度。
  16. Android利用jsoup爬虫爬网页数据(二)
  17. 装X指南之用 Xposed 把某宝资产改成100w
  18. 【计算机网络】第二部分 物理层和介质(4) 数字传输
  19. 华为桌面云虚拟机如何安装Ubuntu 20.04.3-live-server
  20. 【测试】抓包获取浏览器UA,并使用Chrome 调试工具模拟手机

热门文章

  1. Yarn的默认端口(转载)
  2. intellij运行spark的maven方式运行WordCount
  3. 修改win7编码为utf-8
  4. 侯捷面向对象编程C++
  5. 牛客网 在线编程 之字形矩阵打印
  6. 简述php三元运算符的使用,PHP 三元运算符的使用
  7. mysql分隔符声明_MySQL分隔符发生错误时?
  8. PHP无限极分类原理
  9. 针对CDP协议攻击分析及安全防护
  10. 点击panel滚动条滚动到底部