多线程之JMM


写在前面:
笔者才疏学浅,本文参考网上众多前辈大佬的文章,整理引用了部分观点及内容,加之笔者些许愚见糅合而成。
如有侵权还请联系笔者,必在第一时间处理
若是有写的有误,还请务必指出,感激不敬


CPU特性

  1. 由于存储设备与处理设备速度相差较大,因此采用多级缓存提升运算效率
  2. 各处理器优先访问各自的寄存器获取数据,而非到RAM主内存中读取
  3. 为了提升执行效率,处理器会在保证逻辑不变的前提下对指令的执行顺序进行重组

JMM

Java内存模型(Java memory model)用于屏蔽掉各种硬件和操作系统的内存访问差异,以达到跨平台并发的效果。

主内存与工作内存

JMM模型将底层存储结构抽象,制定了如下抽象模型
在这里插入图片描述

常见的线程通信有两种
一是共享内存,线程之间无显式通信,通过修改共享内存来完成通信。
二是消息传递,线程之间发送明确的消息进行显式通信,没有共享内存

Java中采用共享内存的线程通信机制。线程A和线程B之间要完成通信的话,要经历如下两步:

  1. 线程A从主内存中将共享变量读入线程A的工作内存后并进行操作,之后将数据重新写回到主内存中;
  2. 线程B从主存中读取最新的共享变量

此外,根据底层特性可知,多线程执行将带来不同于单线程运行时的问题。由于多级缓存及寄存器的存在,导致了多线程中可见性的问题

重排序

除可见性外,处理器重排序、编译器重排序和内存系统重排序又带来了有序性这一挑战。为保证有序性,JMM包含一系列禁止重排规则。

as-if-serial

对于单线程而言,JMM要求:无论如何重排序,程序执行的结果不能改变。为了达成这一要求,在指令重排时不能破坏原有的数据依赖关系

什么叫数据依赖关系?
如下段代码
int a = 3;
int b = 5;
int c = a + b;
我们称c依赖于a和b,因此c的初始化一定在a b的初始化之后

内存屏障

知道有个这玩意就行了 属于除了面试这辈子都不可能再碰的东西

屏障类型指令示例说明
LoadLoad BarriersLoad1; LoadLoad; Load2确保Load1数据的装载,之前于Load2及所有后续装载指令的装载。
StoreStore BarriersStore1; StoreStore; Store2确保Store1数据对其他处理器可见(刷新到内存),之前于Store2及所有后续存储指令的存储。
LoadStore BarriersLoad1; LoadStore; Store2确保Load1数据装载,之前于Store2及所有后续的存储指令刷新到内存。
StoreLoad BarriersStore1; StoreLoad; Load2确保Store1数据对其他处理器变得可见(指刷新到内存),之前于Load2及所有后续装载指令的装载。StoreLoad Barriers会使该屏障之前的所有内存访问指令(存储和装载指令)完成之后,才执行该屏障之后的内存访问指令。

happens-before

对于单线程来说,无论具体执行过程,其结果一定是正确的,虽然中间执行过程可能五花八门;
但是在多线程的角度看来,每个线程的执行都是花里胡哨,令人????,更不用说得知无依赖关系的指令先后顺序,所以as-if-serial提供的重排约束不足以满足多线程的要求

happens-before是JMM为程序员提供的语义级指令约束,其屏蔽了具体的编译器、处理器的具体排序规则,存在happens-before关系的指令一定可以保证前者对后者可见
但是这里的可见不指明何时可见,可能是第一个操作结束后不久,也可能是程序结束前不久。
此外,两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法。
happens-before共用8项规则

  1. 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  2. 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  3. volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  4. 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
  5. start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
  6. join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
  7. 程序中断规则:对线程interrupted()方法的调用先行于被中断线程的代码检测到中断时间的发生。
  8. 对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始

线程安全

所谓线程安全,指的是多线程下执行结果与预期结果一致

导致线程不安全的原因

这里有两个主要原因:内存不可见 重排序
当CPU1修改了变量A的值,但是只放在了工作内存中,还没来得及刷入主内存,CPU2就读取了主内存中A的值并进行修改,这样就造成了线程安全问题。
而重排序造成线程安全问题,体现在经典的DCL问题

如何解决

  1. 不可变
    例如final,String,Enum这样的,我们从来不考虑线程安全问题,因为他们不能被改动,无论何时读取都是正确的
  2. 互斥同步
    实现互斥锁,如 sync ReentrantLock
  3. CAS
    这里CAS代表的非阻塞同步,实现乐观锁。不采用wait/notify机制,而是循环尝试。详见 原子类

参考链接:

  1. Java内存模型以及happens-before规则
  2. 第十章 进程间的通信 之 Java/Android多线程开发(二)
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值