java并发编程的艺术之内存模型

java的线程是通过共享内存的形式进行通信。那么如何保证线程安全呢?下面主要讨论一些可能会影响线程安全的内存模型、语义。

内存模型基础

  • 内存共享模型
    java内存模型包含主内存和本地内存,共享变量存储在主内存,本地内存是线程对共享变量(即主内存)的副本。
    本地内存是一个抽象概念,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化(硬件和编译器优化可能指的是指令重排序,我觉得作者表达地不准确,大家理解就行)。
    内存模型的抽象结构示意图

    图1:Java内存模型的抽象结构示意图

    线程通信
    图2:线程之间的通信图

  • 指令重排序
    在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序类型如下:
    1)编译器优化:满足as-if-serial下进行重排序
    2)指令集并行:对不存在数据依赖性的语句对应的机器指令进行重叠执行
    3)内存系统:由于CPU使用缓存和读写缓冲区,使得加载和存储操作看上去可能在乱序执行。由于处理器写数据的时候,首相向自己的缓存区写,然后再刷新到内存,所以读取的可能是旧值,需要内存栅栏来解决重排序问题

  • happens before

重排序

顺序一致性

volatile的内存语义

  • 可以实时看到其它线程对vvolatile变量的修改
  • 可以利用内存屏障防止编译器和处理器对代码的重排序:volatile写之前的操作不会被重排序到volatile写之后;volatile读之后的操作不会被重排序到volatile读之前;volatile写、volatile读之间不能进行重排序。具体实现为:每个volatile写之前和之后分别插入StoreStore、StoreLoad屏障,每个volatile读插入LoadLoad、LoadStore屏障。
  • 备注:由于不同处理器的内存模型不一致,内存屏障的插入可以根据具体的处理器再进行优化。

锁的内存语义

锁在释放时,jvm会将本地内存的共享变量刷新到主内存,因此与volatile写具有相同的内存语义;锁在获取时,jvm会将线程对应的本地内存失效,强制从主内存读取数据,因此与volatile读具有相同的内存语义。

  • 内存语义的实现:利用volatile或者cas实现(cas本质上是利用处理器对cmpxchg指令的支持,支持的过程中主要包含对缓冲区对主内存的同步,以及变量的读写前后禁止重排序,即类似volatile变量读写的效果)。

final的内存语义

  • 构造函数对final变量的写入与把构造完的对象引用赋值给其它引用,这两个操作不能重排序。保证对象在对其它线程可见之前,final变量已经被初始化(通过StoreStore屏障)。
  • 初次读一个包含final变量的引用与初次读对象里面的final变量,这两个操作不能重排序。
  • 注意:避免构造的时候出现引用逸出,即构造函数返回之前被构造函数的引用对其它线程可见;读写两者可以重排序。

happen before

happen before其实就是Java内存模型对同步的多线程程序执行结果正确的保证。简单来说,多线程中,操作A happens before 操作B,即操作A的结果对操作B可见。

总结

JMM需要屏蔽不同处理器的差异,为开发者呈现一致的内存模型。
另外,无论是处理器,还是JMM,都需要在并发性能与并发控制处理上进行取舍和平衡。并发性能越好,意味着并发控制越复杂,编程的难度越大。反之,并发控制简单,就无法充分利用处理器的性能。

疑问

如果有疑问,欢迎讨论。

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值