三.JVM与线程的原子性,可见性,有序性,易变性

1.硬件内存模型

硬件内存模型: 处理器->高速缓存->缓存一致性协议->主存
在这里插入图片描述

一:高速缓存的价值
  • CPU的运行效率要远远高于ARM也就是我们的主存,那么在这样的一个大前提下,当CPU所需要的数据需要从ARM中读取时,相对来说CPU有很大一部分性能被闲置,并未发挥到CPU最大效能
  • 所以基于前一点,加入高速缓存后,高速缓存的运行效率与CPU接近,锁需要的数据从高速缓存获取,这样一来,尽可能的避免了CPU被闲置的情况,从而从整体上提高了程序的执行效率
二:高速缓存运作原理
  • 高速缓存会将在CPU执行所需要的数据从ARM中拷贝写入到高速缓存中,那么在对高速缓存进程操作的时候会有两种情况:
    • 高速缓存被命中 : 在在CPU执行时,对于所需要的数据首先会去与之性能最接近的高速缓冲中寻找,当在高速缓存中找到后就直接执行,那么在这样的情况下就凸显出了高速缓存的价值
    • 高速缓存未被命中 : 在在CPU执行时,对于所需要的数据首先会去与之性能最接近的高速缓冲中寻找,当未找到,CPU被迫等待,后续由高速缓存从ARM中拷贝再从高速缓存读取
三:高速缓存的回收机制

假如高速缓内存空间已满,那么所需要做的就是清理置空一部分高速缓存的内存空间,为后续使用提供空间,最常见的一种策略就是LRU原则(最近最少使用原则),该原则在java的LinkedHashMap就有使用,其底层实现的原理就是LRU原则

四:高速缓存与命中率

CPU在从高速缓存中读取数据之前,那么就先必须找到所需要的数据它在高速缓存上的位置,而查找又需要时间,由此一来就延伸出了一个问题: 在高速缓存上的寻找效率和命中率

  • 直接映射: 高速缓存只提供一块缓存给ARM中提供的数据进行存储,那么CPU只需要直接查找这个位置所需要的数据是否存在就可,但这也存在着一个缺陷就是一个缓存快的容量非常有限,很大程度上限制了ARM所提供的数据量,这样一来,尽管提高了寻找效率但很大程度上降低了命中率

  • 关联映射: 与直接映射完全相反, 高速缓存中的任意缓存块都可提供给ARM的数据进行存储,而这样一来,这极大的提高了命中率,但也增加了CPU寻找的时间

  • 组相联映射 : 这是上面二者之间比较折中的方案,提供数量有限的缓存块用来存储ARM中提供的数据,例如:2路相联映射系统将ARM中提供的数据存储到两个中的任一一个缓存块中,8路相联映射系统就是将ARM中提供的数据存储到8个中的任一一个缓存块中进行存储

五.缓存的写策略
  • "write-through"策略:该策略下,任何写入高速缓存的数据都会直接写入RAM
  • “write-back策略”: 任何写进高速缓存的数据都会被标记为"dirty",当该数据从高速缓存中删除时,才会写入RAM刷新RAM中原来的数据,在该策略下会导致多线程中对数据的不可见性
六:缓存一致性协议

该协议的主要作用就是在多核处理器上,每个核心对于共享数据保证数据一致性的处理解决方案,由于篇幅过长,推荐大家可看此处: 缓存一致性协议

七:多线程与多核
  • 多核心处理器是指:在一个处理器上集成多个运算处理核心,从而提高计算能力,也就
    有多个真正具有并行计算的核心,每一个处理核心,对应一个内核
    线程.

  • 内核线程:就是直接由操作系统内核支持的线程,内核线程是由内核来完成
    切换的,而内核通过操作调度器对线程完成调度,内核并且负责
    将线程的任务映射到各个处理器上.

一般来说,一个内核对应一条内核线程,但现在一般的CPU,都是
一个处理核心对应两条内核线程,其所采用的是超级线程技术,
将一个物理处理核心虚拟成两个逻辑处理核心,来实现让单个处理核心有也具有线程并行运算能力.

程序一般不会直接去使用内核线程,而是去使用内核线程的高级
接口—轻量级进程,也就是我们常说的 用户线程,每一个用户
线程都会有一个内核线程,所以需要先对内核线程支持,才会有


2.Java内存模型

规范了线程如何和何时可以看到其他线程修改过的共享变量的值,以及在必须时如何同步的访问共享变量

在这里插入图片描述
在这里插入图片描述
注:图2引用于百度图库

一:工作内存

工作内存是java模型中的一个抽象概念,用来指定代表CPU的寄存器和高速缓存等

二:本地内存及其私有化性质
  • 同工作内存一样是一个抽象描述
  • 从图中我们可以看到,在线程执行时,会对数据所需要的数据进行私有拷贝作为副本
  • 所需要注意:当两个线程中的两个方法同时指向堆内存中的同一个成员变量的时候,这时两个线程都能够对该成员变量进行 私有拷贝作为副本,各线程在执行的都可以对该成员变量的副本进行操作,而这过程中,各线程中该副本的值可能发生变化,这是导致多线程安全问题的本质
  • 由上图2可以到,调用栈和本地变量都是存放线程栈中,执行过程中所调用的的方法也放在线程栈中
三:堆

堆: 运行时的数据区,堆是由垃圾回收器来负责的,堆的,创建的对象和静态变量,成员变量就放在堆中

  • 优势:动态的分配内存大小,由Java垃圾回收器动态回收
  • 缺点: 需要存储时,因动态的分配内存大小,所以存储速度会慢一些
四:栈

栈: 栈中主要存放一些基本类型的变量,例如 int short,long…等;

  • 优势: 存储速度比堆要快,栈中数据可以共享
  • 缺点: 存在栈中的数据大小与生命周期是固定的,如java类中编写的局部变量等
五:java内存间的交互操作

在这里插入图片描述
注:图引用于百度图库

  1. lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态
  2. unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
  3. read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
  4. load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
  5. use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
  6. assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量
  7. store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作
  8. write(写入):作用于主内存的变量,它把store操作从工作内存中的一个变量的值传送到主内存的变量中
六:内存间的交互操作需要满足的八条规则
  1. 不允许read和load,store和write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作内存发起回写了但主内存不接受的情况出现.
  2. 不允许一个线程丢弃它的最近的assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存.
  3. 不允许一个线程无原因地(没有发生过任何assign操作)把数据从线程的工作内存同步回主内存.
  4. 一个新的变量只能在主内存中"诞生",不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,换句话说,就是对一个变量实施use,store操作之前,必须先执行过了assign和load操作.
  5. 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁.
  6. 如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值.这就使得线程加锁时使用共享变量时需要从主内存中重新读取最新的值
  7. 如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去unlock一个被其他线程锁定住的变量.
  8. 对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store,write操作).

注:该规则引用于此处


3.原子性

一:什么是原子性
  • 一个代码块在一次执行过程中,该代码块要就全部执行完成,要就不执行.在执行该代码块的时候不存在多线程中执行到一半切换线程去执行其他逻辑
二:synchronize,Lock保障原子性
  • synchronize: 依赖JVM
  • Lock: 依赖特殊的CPU指令

4.可见性

  • 可以理解为:当前线程在对主内存的数据操作后更新,能够保证在主内存中,该数据在被其他线程访问之前刷新到主内存
一:不可见的原因
  1. 多线程中CPU切换执行每条线程中的代码
  2. 多线程中CPU切换执行每条线程中的代码且代码在编译过程中受编译器重排序(重排序后面会讲到)
  3. 由于高速缓存写入主内存的策略不同,例如在"write-back"策略下,变量的更新并不会在第一时间更新到主内存,而是当它从高速缓存中删除时,才会写入主内存中,这样就导致了数据更新后对其他线程的不可见性
二:synchronize 可见性
  1. 由上面内存间的交互操作需要满足的八条规则第六条,可以知道,线程加锁时,将清空工作内存中共享变量的值,从而使共享变量的值需要使用时从主内存中获取最新的值(注意:加锁与解锁是同一把锁)
  2. 由上面内存间的交互操作需要满足的八条规则第八条,可以知道,线程解锁前,必须把共享变量最新值更新到主内存.
三:volatile 关键字
  1. 被volatile关键字修饰,在编译时期,能够防止编译器对其只能重排序
  2. 被volatile关键字修饰的变量每次被访问,都会强制从主内存中读取,当变量发生变化的时候都会强制将最新的值写入主内存中.保证了volatile变量的可见性
  3. 在多线程的环境下,被volatile关键字修饰的变量不能保证其执行的原子性

5.有序性

一:先行发生原则 happens-before
  • 规范限定了代码执行的先后顺数,是代码执行顺序的主要依据
先行发生原则(8种)
  1. 程序次序规则:同一个线程内,按照代码出现的顺序,前面的代码先行于后面的代码,准确的说是控制流顺序,因为要考虑到分支和循环结构.

  2. 管程锁定规则:一个unlock操作先行发生于后面(时间上)对同一个锁的lock操作.

  3. volatile变量规则:对一个volatile变量的写操作先行发生于后面(时间上)对这个变量的读操作.

  4. 线程启动规则:Thread的start()方法先行发生于这个线程的每一个操作.

  5. 线程终止规则:线程的所有操作都先行于此线程的终止检测.可以通过Thread.join()方法结束,Thread.isAlive()的返回值等手段检测线程的终止.

  6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupt()方法检测线程是否中断

  7. 对象终结规则:一个对象的初始化完成先行于发生它的finalize()方法的开始.

  8. 传递性:如果操作A先行于操作B,操作B先行于操作C,那么操作A先行于操作C.

注:该原则引用于此处

二:该原则注意事项

注意:

1. 不安全线程导致的情况和先行发生原则就有关系,因为两个线程调用同一个变量,两个操作不在罗列出来的八种规则里面,那么就可以对他们进行任意的重排序,从而导致了线程安全问题. 该问题也是下面要讲到的指令重排序问题

  • 时间先后顺序与先行发生的顺序之间基本没有太大的关系.

6.易变性(指令重排序)

一:什么是指令重排序?

重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段.

二:数据依赖性
  • 在单线程下,访问两变量,其中至少一个变量有写操作,此时就存在数据依赖性

存在数据依赖性,例如:

  • 写后读 a=0 b=a;
  • 读后写 a=b b=1;
  • 写后写 a=1 a=2

注意: 上面三个例子的有效性仅仅局限于单线程中,在多线程下交叉执行可能出现问题;

例:

int a=1,b=1;
        new Thread(() -> {
        a=0; b=a;
        System.out.println("b:"+ b);
        });
        new Thread(() -> {
        a=0; b=a;
        System.out.println("b:"+ b);
        });

正确b=0 错误b=1
三:指令重排序举例
  • 编译器和处理器在重排序时,会遵守数据依赖性,编译器和处理器不会对存在数据依赖关系的两个变量进行重排序.而数据依赖性基于单线程考虑,在多线程不受数据依赖性限制,则可能导致指令重排序举例;

例:

static int a = 0, b= 0 ,x = 0, y = 0;
...

        new Thread(() -> {
            a = 1;//1
            x = b;//2
        });
        new Thread(() -> {
            b = 1;//3
            y = a;//4
        });

        System.out.println("x:"+ x+";  y:"+ y);
        //当未指令重排序正常执行结果为: x = 0; y = 1;
        //当指令重排序按上面标注,执行顺序为2,3,4,1结果为: x = 0; y = 0;

由此可以总结到:多线程下,代码书写的前后顺序并不能够完全保证代码的先后执行顺序

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值