并发和并行

目标都是最大化CPU的使用率

并行(parallel)

指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是 从宏观来看,二者都是一起执行的。 image.png

并发(concurrency)

指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。
image.png
并发解决的问题场景:
多线程之间同步(线程之间存在的依赖关系),互斥(共享资源只能某个线程独享),分工(比如计算很大一个数组的和,拆成很多小的数组进行合并)的问题。

CPU高速缓存(Cache Memory)

CPU缓存即高速缓冲存储器,是位于CPU与主内存间的一种容量较小但速度很高的存储器。由于CPU的速度远高于主内存,CPU直接从内存中存取数据要等待一定时间周期,Cache中保存着CPU刚用过或循环使用的一部分数据,当CPU再次使用该部分数据时可从Cache中直接调用,减少CPU的等待时间,提高了系统的效率。
image.png
在CPU访问存储设备时,无论是存取数据或者存取指令,都趋于聚集在一片连续的区域中,这就是局部性原理。
时间局部性(Temporal Locality):如果一个信息项正在被访问,那么在近期它很可能还会被再次访问。 比如循环、递归、方法的反复调用等。
空间局部性(Spatial Locality):如果一个存储器的位置被引用,那么将来他附近的位置也会被引用。比如顺序执行的代码、连续创建的两个对象、数组等。

多CPU多核缓存架构

物理CPU:物理CPU就是插在主机上的真实的CPU硬件,在Linux下可以数不同的physical id来确认主机的物理CPU个数。
核心数:我们常常会听说多核处理器,其中的核指的就是核心数。在Linux下可以通过cores来确认主机的物理CPU的核心数。
逻辑CPU:逻辑CPU跟超线程技术有联系,假如物理CPU不支持超线程的,那么逻辑CPU的数量等于核心数的数量;如果物理CPU支持超线程,那么逻辑CPU的数目是核心数数目的两倍。在Linux下可以通过processors的数目来确认逻辑CPU的数量。现代CPU为了提升执行效率,减少CPU与内存的交互,一般在CPU上集成了多级缓存架构,常见的为三级缓存结构。
image.png

缓存行(Cache Line)

CPU操作缓存的单位是缓存行,也就是说如果CPU要读一个变量x,那么其实是读变量x所在的整个缓存行。缓存行的大小是64Byte

缓存一致性(Cache Coherence)

计算机体系结构中,缓存一致性是共享资源数据的一致性,这些数据最终存储在多个本地缓存中。当系统中的客户机维护公共内存资源的缓存时,可能会出现数据不一致的问题,这在多处理系统中的cpu中尤其如此。
image.png
在共享内存多处理器系统中,每个处理器都有一个单独的缓存内存,共享数据可能有多个副本:一个副本在主内存中,一个副本在请求它的每个处理器的本地缓存中。当数据的一个副本发生更改时,其他副本必须反映该更改。缓存一致性是确保共享操作数(数据)值的变化能够及时地在整个系统中传播的过程。

缓存一致性的要求

写传播(Write Propagation)
对任何缓存中的数据的更改都必须传播到对等缓存中的其他副本(该缓存行的副本)。
事务串行化(Transaction Serialization)
对单个内存位置的读/写必须被所有处理器以相同的顺序看到。理论上,一致性可以在加载/ 存储粒度上执行。然而,在实践中,它通常在缓存块的粒度上执行。
一致性机制(Coherence mechanisms)
确保一致性的两种最常见的机制是窥探机制(snooping)和基于目录的机制(directory based),这两种机制各有优缺点。如果有足够的带宽可用,基于协议的窥探往往会更快,因为所有事务都是所有处理器看到的请求/响应。其缺点是窥探是不可扩展的。每个请求都必须广播到系统中的所有节点,这意味着随着系统变大,(逻辑或物理)总线的大小及其提供的带宽也必须增加。另一方面,目录往往有更长的延迟(3跳 请求/转发/响应),但使用更少的带宽,因为消息是点对点的,而不是广播的。由于这个原因,许多较大的系统(>64处理器)使用这种类型的缓存一致性。

总线仲裁(Bus Arbitration)

在计算机中,数据通过总线在处理器和内存之间传递。每次处理器和内存之间的数据传递都
是通过一系列步骤来完成的,这一系列步骤称之为总线事务(Bus Transaction)。总线事务包
括读事务(Read Transaction)和写事务(WriteTransaction)
image.png
假设处理器A,B和C同时向总线发起总线事务,这时总线仲裁会对竞争做出裁决,这里假设总线在仲裁后判定处理器A在竞争中获胜(总线仲裁会确保所有处理器都能公平的访问内存)。此时处理器A继续它的总线事务,而其他两个处理器则要等待处理器A的总线事务完成后才能再次执行内存访问。假设在处理器A执行总线事务期间(不管这个总线事务是读事务还是写事务),处理器D向总线发起了总线事务,此时处理器D的请求会被总线禁止。
总线的这种工作机制可以把所有处理器对内存的访问以串行化的方式来执行。在任意时间点,最多只能有一个处理器可以访问内存。这个特性确保了单个总线事务之中的内存读/写操作具有原子性(多个总线事务无法保证,需要总线锁定和缓存锁定保证)。

原子操作是指不可被中断的一个或者一组操作。处理器会自动保证基本的内存操作的原子性,也就是一个处理器从内存中读取或者写入一个字节时,其他处理器是不能访问这个字节的内存地址。最新的处理器能自动保证单处理器对同一个缓存行里进行16/32/64位的操作是原子 的,但是复杂的内存操作处理器是不能自动保证其原子性的,比如跨总线宽度、跨多个缓存行和 跨页表的访问。处理器提供总线锁定缓存锁定两个机制来保证复杂内存操作的原子性。

总线锁定

总线锁定就是使用处理器提供的一个LOCK#信号,当其中一个处理器在总线上输出此信号 时,其它处理器的请求将被阻塞住,那么该处理器可以独占共享内存(只有当读写完成刷回主内存后才会释放总线锁)。

缓存锁定

由于总线锁定阻止了被阻塞处理器和所有内存之间的通信,而输出LOCK#信号的CPU可能只需要锁住特定的一块内存区域,因此总线锁定开销较大。
缓存锁定(可由LOCK前缀指令触发)是指内存区域如果被缓存在处理器的缓存行中,并且在Lock操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不会在总线上声言LOCK#信号(总线锁定信号),而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改由两个及以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效。
缓存锁定不能使用的特殊情况:
1.当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行时,则处理器会调用总线锁定(LOCK#信号);
2.有些处理器不支持缓存锁定。

总线窥探(Bus Snooping)

总线窥探是缓存中的一致性控制器(snoopy cache)监视或窥探总线事务的一种方案,其目标是在分布式共享内存系统中维护缓存一致性。包含一致性控制器(snooper)的缓存称为snoopy缓存。该方案由Ravishankar和Goodman于1983年提出。

工作原理

当特定数据被多个缓存共享时,处理器修改了共享数据的值,更改必须传播到所有其他具有该数据副本的缓存中。这种更改传播可以防止系统违反缓存一致性。数据变更的通知可以通过总线窥探来完成。所有的窥探者都在监视总线上的每一个事务。如果一个修改共享缓存块的事务出现在总线上,所有的窥探者都会检查他们的缓存是否有共享块的相同副本。如果缓存中有共享块的副本,则相应的窥探者执行一个动作以确保缓存一致性。这个动作可以是刷新缓存块或使缓存块失效。它还涉及到缓存块状态的改变,这取决于缓存一致性协议(cache coherence protocol)。

窥探协议类型

根据管理写操作的本地副本的方式,有两种窥探协议:
Write-invalidate:写时失效-即让其他缓存失效
当处理器写入一个共享缓存块时,其他缓存中的所有共享副本都会通过总线窥探失效。这种方法确保处理器只能读写一个数据的一个副本。其他缓存中的所有其他副本都无效。这是最常用的窥探协议。(MSI、MESI、MOSI、MOESI和MESIF协议属于该类型)
Write-update:写时更新-即将写数据广播到其他缓存中
当处理器写入一个共享缓存块时,其他缓存的所有共享副本都会通过总线窥探更新。这个方法将写数据广播到总线上的所有缓存中。它比write-invalidate协议引起更大的总线流量。这就是为什么这种方法不常见。(Dragon和firefly协议属于此类别)

一致性协议(Coherence protocol)

一致性协议在多处理器系统中应用于高速缓存一致性。为了保持一致性,人们设计了各种模型和协议,如MSI、MESI(又名Illinois)、MOSI、MOESI、MERSI、MESIF、write-once、Synapse、Berkeley、Firefly和Dragon协议。—不同cpu可能不一样
MESI协议—一致性协议中比较常见的一种
MESI协议是一个基于写失效的缓存一致性协议,是支持回写(write-back)缓存的最常用协议。也称作伊利诺伊协议(Illinois protocol,因为是在伊利诺伊大学厄巴纳-香槟分校被发明的)。与写通过(write through)缓存相比,回写缓冲能节约大量带宽。总是有“脏”(dirty)状态表示缓存中的数据与主存中不同。MESI协议要求在缓存不命中(miss) 且数据块在另一个缓存时,允许缓存到缓存的数据复制。与MSI协议相比,MESI协议减少了主 存的事务数量。这极大改善了性能。
4种状态:
已修改Modified (M)
缓存行是脏的(dirty),与主存的值不同。如果别的CPU内核要读主存这块数据,该缓存行必须回写到主存,状态变为共享(S).
独占Exclusive (E)
缓存行只在当前缓存中,但是是干净的—缓存数据同于主存数据。当别的缓存读取它时,状态变为共享;当前写数据时,变为已修改状态。
共享Shared (S)
缓存行也存在于其它缓存中且是未修改的。缓存行可以在任意时刻抛弃。
无效Invalid (I)
缓存行是无效的

Java内存模型(JMM)

Java虚拟机规范中定义了Java内存模型(Java Memory Model,JMM),用于屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的并发效果,JMM规范了Java虚拟机与计算机内存是如何协同工作的:规定了一个线程如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。JMM描述的是一种抽象的概念,一组规则,通过这组规则控制程序中各个变量在共享数据区域和私有数据区域的访问方式,JMM是围绕原子性、有序性、可见性展开的。
image.png
内存交互操作
关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成:
lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用。
load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值
传送到主内存的变量中。

并发三大特性

并发编程Bug的源头:可见性、原子性和有序性问题

内存屏障

X86的memory barrier指令包括lfence(读屏障),sfence(写屏障),mfence(全屏障)
1、Store Memory Barrier(写屏障):告诉处理器在写屏障之前的所有已经存储在存储缓存(store bufferes)中的数据同步到主内存,简单来说就是使得写屏障之前的指令的结果对屏障之后的读或者写是可见的,可理解为立马同步到主内存
2、Load Memory Barrier(读屏障):处理器在读屏障之后的读操作,都在读屏障之后执行。配合写屏障,使得写屏障之前的内存更新对于读屏障之后的读操作是可见的,可理解为直接读取主内存
3、Full Memory Barrier(全屏障):确保屏障前的内存读写操作的结果提交到内存之后,再执行屏障后的读写操作。

lock前缀指令

通过lock前缀指令,会锁定变量缓存行区域并写回主内存,这个操作称为“缓存锁定”,缓存一致性协议会阻止同时修改被两个以上处理器缓存的内存区域数据。一个处理器的缓存回写到内存会导致其他处理器的缓存无效。
作用:
1. 确保后续指令执行的原子性。在Pentium及之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其它处理器暂时无法通过总线访问内存,很显然,这个开销很大。在新的处理器中,Intel使用缓存锁定来保证指令执行的原子性,缓存锁定将大大降低lock前缀指令的执行开销。
2. LOCK前缀指令具有类似于内存屏障的功能,禁止该指令与前面和后面的读写指令重排序。
3. LOCK前缀指令会等待它之前所有的指令完成、并且所有缓冲的写操作写回内存(也就是将store buffer中的内容写入内存)之后才开始执行,并且根据缓存一致性协议,刷新store buffer的操作会导致其他cache中的副本失效。
在linux系统x86中的实现
orderAccess_linux_x86.inline.hpp

  1. inline void OrderAccess::storeload() { fence(); }
  2. inline void OrderAccess::fence() {
  3. if (os::is_MP()) {
  4. // always use locked addl since mfence is sometimes expensive
  5. #ifdef AMD64
  6. __asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
  7. #else
  8. __asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
  9. #endif
  10. }
  11. }

也就是说内存屏障在底层增加了lock前缀指令来达到效果的。

可见性

当一个线程修改了共享变量的值,其他线程能够看到修改的值。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方法来实现可见性的。
可见性代码如下:

public class VisibilityTest {
    // storeLoad  JVM内存屏障  ---->  (汇编层面指令)  lock; addl $0,0(%%rsp)
    // lock前缀指令不是内存屏障的指令,但是有内存屏障的效果   缓存失效
    private boolean flag = true;
    private int count = 0;

    public void refresh() {
        flag = false;
        System.out.println(Thread.currentThread().getName() + "修改flag:"+flag);
    }

    public void load() {
        System.out.println(Thread.currentThread().getName() + "开始执行.....");
        while (flag) {
            //TODO  业务逻辑
            count++;
            //JMM模型    内存模型: 线程间通信有关   共享内存模型
            //没有跳出循环   可见性的问题
            //能够跳出循环   内存屏障
            //UnsafeFactory.getUnsafe().storeFence();
            //能够跳出循环    ?   释放时间片,上下文切换   加载上下文:flag=true
            //Thread.yield();
            //能够跳出循环    内存屏障
            //System.out.println(count);

            //LockSupport.unpark(Thread.currentThread());

            //shortWait(1000000); //1ms
            //shortWait(1000);

//            try {
//                Thread.sleep(1);   //内存屏障
//            } catch (InterruptedException e) {
//                e.printStackTrace();
//            }

            //总结:  Java中可见性如何保证? 方式归类有两种:
            //1.  jvm层面 storeLoad内存屏障    ===>  x86   lock替代了mfence
            // 2.  上下文切换   Thread.yield();


        }
        System.out.println(Thread.currentThread().getName() + "跳出循环: count=" + count);
    }

    public static void main(String[] args) throws InterruptedException {
        VisibilityTest test = new VisibilityTest();

        // 线程threadA模拟数据加载场景
        Thread threadA = new Thread(() -> test.load(), "threadA");
        threadA.start();

        // 让threadA执行一会儿
        Thread.sleep(1000);
        // 线程threadB通过flag控制threadA的执行时间
        Thread threadB = new Thread(() -> test.refresh(), "threadB");
        threadB.start();

    }

    public static void shortWait(long interval) {
        long start = System.nanoTime();
        long end;
        do {
            end = System.nanoTime();
        } while (start + interval >= end);
    }
}

image.png

通过内存屏障保证可见性

UnsafeFactory.getUnsafe().loadFence(); //调用读屏障   lfence(读屏障)
UnsafeFactory.getUnsafe().storeFence(); //调用写屏障  sfence(写屏障)
UnsafeFactory.getUnsafe().fullFence(); //调用全屏障   mfence(全屏障)

会调用本地方法:
image.png
底层添加lock前缀指令,保证可见性

通过final关键字保证可见性

image.png
Integer底层包含final关键字,也提供了内存屏障的规则

通过synchronized关键字保证可见性

image.png
println()的底层是用的synchronized:
image.png
synchronized编译成字节码后,是通过monitorenter(入锁)和monitorexit(出锁)两个指令实现的,具体过程如下:
JMM&并发三大特性 - 图13
可以发现,与volatile类似,synchronized底层也是通过释放屏障和获取屏障的配对使用保障有序性,加载屏障和存储屏障的配对使用保障可见性。最后又通过锁的排他性保障了原子性。

通过volatile关键字保证可见性

image.png
会调用内存屏障(写屏障),即用内存屏障来实现可见性
汇编层面volatile的实现
image.png
验证了可见性使用了lock前缀指令

通过上下文切换保证可见性

Thread.yield();  //让出时间片,会有上下文切换

让出时间片,上下文切换(保存现场,还原现场时会加载上下文),会再次从主存中去加载最新的值,保证可见性

注:保证可见性从底层方式归类有2种:
1.JVM层面内存屏障storeFence,loadFence—>x86汇编层面lock前缀指令替代了mfence
2.上下文切换
image.png

原子性

一个或多个操作,要么全部执行且在执行过程中不被任何因素打断,要么全部不执行。在Java中,对基本数据类型的变量的读取和赋值操作是原子性操作(64位处理器)。不采取任何的原子性保障措施的自增操作并不是原子性的。
在java中,对基本数据类型的变量的读取和赋值操作是原子性操作有点要注意的是,对于32位系统的来说,long类型数据和double类型数据(对于基本数据类型,byte,short,int,float,boolean,char读写是原子操作),它们的读写并非原子性的,也就是说如果存在两条线程同时对long类型或者double类型的数据进行读写是存在相互干扰的,因为对于32位虚拟机来说,每次原子读写是32位的,而long和double则是64位的存储单元,这样会导致一个线程在写时,操作完前32位的原子操作后,轮到B线程读取时,恰好只读取到了后32位的数据,这样可能会读取到一个既非原值又不是线程修改值的变量,它可能是“半个变量”的数值,即64位数据被两个线程分成了两次读取。但也不必太担心,因为读取到“半个变量”的情况比较少见,至少在目前的商用的虚拟机中,几乎都把64位的数据的读写操作作为原子操作来执行,因此对于这个问题不必太在意,知道这么回事即可。
除了JVM自身提供的对基本数据类型读写操作的原子性外,可以通过 synchronized和Lock实现原子性。因为synchronized和Lock能够保证任一时刻只有一个线程访问该代码块。

如何保证原子性

通过 synchronized 关键字保证原子性。
通过 Lock保证原子性。
通过 CAS保证原子性。

有序性

即程序执行的顺序按照代码的先后顺序执行。JVM 存在指令重排,所以存在有序性问题。

指令重排序

Java语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。指令重排序的意义:JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。
指令重排的阶段:在编译器与CPU处理器中都能执行指令重排优化操作
JMM&并发三大特性 - 图17

volatile重排序规则

JMM&并发三大特性 - 图18
volatile禁止重排序场景:
1. 第二个操作是volatile写,不管第一个操作是什么都不会重排序
2. 第一个操作是volatile读,不管第二个操作是什么都不会重排序
3. 第一个操作是volatile写,第二个操作是volatile读,也不会发生重排序

JMM内存屏障插入策略

  1. 在每个volatile写操作的前面插入一个StoreStore屏障
    2. 在每个volatile写操作的后面插入一个StoreLoad屏障
    3. 在每个volatile读操作的后面插入一个LoadLoad屏障
    4. 在每个volatile读操作的后面插入一个LoadStore屏障
    JMM&并发三大特性 - 图19
    如何充分压榨硬件性能,压榨CPU计算能力,减少CPU等待时间(机械同感)
    x86处理器不会对读-读、读-写和写-写操作做重排序, 会省略掉这3种操作类型对应的内存屏障。仅会对写-读操作做重排序,所以volatile写-读操作只需要在volatile写后插入StoreLoad屏障
    JMM&并发三大特性 - 图20
    JVM层面的内存屏障
    在JSR规范中定义了4种内存屏障:
    LoadLoad屏障:(指令Load1; LoadLoad; Load2),在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
    LoadStore屏障:(指令Load1; LoadStore; Store2),在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
    StoreStore屏障:(指令Store1; StoreStore; Store2),在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
    StoreLoad屏障:(指令Store1; StoreLoad; Load2),在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。【在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能】
    由于x86只有store load可能会重排序,所以只有JSR的StoreLoad屏障对应它的mfence或lock前缀指令,其他屏障对应空操作
    硬件层内存屏障
    硬件层提供了一系列的内存屏障 memory barrier / memory fence(Intel的提法)来提供一致性的能力。拿X86平台来说,有几种主要的内存屏障:
    1. lfence,是一种Load Barrier 读屏障
    2. sfence, 是一种Store Barrier 写屏障
    3. mfence, 是一种全能型的屏障,具备lfence和sfence的能力
    4. Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。它后面可以跟ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG等指令。
    内存屏障有两个能力:
    1. 阻止屏障两边的指令重排序
    2. 刷新处理器缓存/冲刷处理器缓存
    对Load Barrier来说,在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主内存加载数据;对Store Barrier来说,在写指令之后插入写屏障,能让写入缓存的最新数据写回到主内存。
    Lock前缀实现了类似的能力,它先对总线和缓存加锁,然后执行后面的指令,最后释放锁后会把高速缓存中的数据刷新回主内存。在Lock锁住总线的时候,其他CPU的读写请求都会被阻塞,直到锁释放
    不同硬件实现内存屏障的方式不同,Java内存模型屏蔽了这种底层硬件平台的差异,由JVM来为不同的平台生成相应的机器码。

    如何保证有序性

    通过 volatile 关键字保证有序性。
    通过 内存屏障保证有序性。
    通过 synchronized关键字保证有序性。
    通过 Lock保证有序性。

    伪共享的问题

    如果多个核的线程在操作同一个缓存行中的不同变量数据,那么就会出现频繁的缓存失效,即使在代码层面看这两个线程操作的数据之间完全没有关系。这种不合理的资源竞争情况就是伪共享(False Sharing)。
    JMM&并发三大特性 - 图21
    linux下查看Cache Line大小
    Cache Line大小是64Byte
    JMM&并发三大特性 - 图22
    或者执行 cat /proc/cpuinfo 命令
    JMM&并发三大特性 - 图23
    避免伪共享方案
    1.缓存行填充

    class Pointer {
     volatile long x;
     //避免伪共享: 缓存行填充
     long p1, p2, p3, p4, p5, p6, p7;
     volatile long y;
    }
    

    2.使用 @sun.misc.Contended 注解(java8)
    注意需要配置jvm参数:-XX:-RestrictContended ```java public class FalseSharingTest {

    public static void main(String[] args) throws InterruptedException {

     testPointer(new Pointer());
    

    }

    private static void testPointer(Pointer pointer) throws InterruptedException {

     long start = System.currentTimeMillis();
     Thread t1 = new Thread(() -> {
         for (int i = 0; i < 100000000; i++) {
             pointer.x++;
         }
     });
    
     Thread t2 = new Thread(() -> {
         for (int i = 0; i < 100000000; i++) {
             pointer.y++;
         }
     });
    
     t1.start();
     t2.start();
     t1.join();
     t2.join();
    
     System.out.println(pointer.x+","+pointer.y);
    
     System.out.println(System.currentTimeMillis() - start);
    

    } }

class Pointer { // 避免伪共享: @Contended + jvm参数:-XX:-RestrictContended jdk8支持 //@Contended volatile long x; //避免伪共享: 缓存行填充 //long p1, p2, p3, p4, p5, p6, p7; volatile long y; }

<a name="C0Bx2"></a>
## as-if-serial  
as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。  
```java
double pi = 3.14; // A
double r = 1.0; // B
double area = pi * r * r; // C

A和C之间存在数据依赖关系,同时B和C之间也存在数据依赖关系。因此在最终执行的指令序列中,C不能被重排序到A和B的前面(C排到A和B的前面,程序的结果将会被改变)。但A和B之间没有数据依赖关系,编译器和处理器可以重排序A和B之间的执行顺序。

happens-before

从JDK 5 开始,JMM使用happens-before的概念来阐述多线程之间的内存可见性。在JMM 中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happensbefore关系。 happens-before和JMM关系如下图
image.png
下面是happens-before原则规则(8条):
1.程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操 作;
2.锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作;
3.volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作;
4.传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A 先行发生于操作C; 5.线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作;
6.线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件 的发生;
7.线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过 Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行;
8.对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始;
推论(6条):
1.将一个元素放入一个线程安全的队列的操作Happens-Before从队列中取出这个元素的操作
2.将一个元素放入一个线程安全容器的操作Happens-Before从容器中取出这个元素的操作
3.在CountDownLatch上的倒数操作Happens-Before CountDownLatch#await()操作
4.释放Semaphore许可的操作Happens-Before获得许可操作
5.Future表示的任务的所有操作Happens-Before Future#get()操作
6.向Executor提交一个Runnable或Callable的操作Happens-Before任务开始执行操作

问题一:简述并发和并行的区别

并行(parallel):指在同一时刻,有多条指令在多个处理器上同时执行。所以无论从微观还是从宏观来看,二者都是一起执行的。
并发(concurrency):指在同一时刻只能有一条指令执行,但多个进程指令被快速的轮换执行,使得在宏观上具有多个进程同时执行的效果,但在微观上并不是同时执行的,只是把时间分成若干段,使多个进程快速交替的执行。 并行在多处理器系统中存在,而并发可以在单处理器和多处理器系统中都存在。

问题二:谈谈对JMM模型的理解

因为在不同的硬件生产商和不同的操作系统下,内存的访问有一定的差异,所以会造成相同的代码运行在不同的系统上会出现各种问题。所以java内存模型(JMM)屏蔽掉各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的并发效果。
Java内存模型规定所有的变量都存储在主内存中,包括实例变量,静态变量,但是不包括局部变量和方法参数。每个线程都有自己的工作内存,线程的工作内存保存了该线程用到的变量和主内存的副本拷贝,线程对变量的操作都在工作内存中进行。线程不能直接读写主内存中的变量。不同的线程之间也无法访问对方工作内存中的变量。线程之间变量值的传递均需要通过主内存来完成。

问题三:谈谈伪共享问题和其解决方案

如果多个核的线程在操作同一个缓存行中的不同变量数据,那么就会出现频繁的缓存失效,即使在代码层面看这两个线程操作的数据之间完全没有关系。这种不合理的资源竞争情况就是伪共享(False Sharing)。
避免伪共享方案 :
1.缓存行填充
2.使用 @sun.misc.Contended 注解(java8)

问题四:聊聊你对as-if-serial和happens-before的理解

as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。
从JDK 5开始,JMM使用happens-before的概念来阐述多线程之间的内存可见性。在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须存在happens-before关系。具体的规则(8条规则 + 6条推论)了解就行。happens-before原则是JMM中非常重要的原则,它是判断数据是否存在竞争、线程是否安全的主要依据,保证了多线程环境下的可见性。