Java并发编程之Java内存模型 (上)

1. 前言

在看完《Java并发编程的艺术》这本书的第三章后,觉得受益颇多。特别是对关于几个能够实现线程安全的关键字的内存模型的解析尤为透彻,所有在这里写个总结。

2. 内存模型基础

2.1 概述

Java线程之间的通信对应程序员来说是透明的,关于内存可见性问题对应程序开发者来说,是极其烦恼的一个问题。如果不了解Java线程之间的通信机制,那么在并发编程中犯容易犯线程不同步错误,从而导致线程的安全性问题。

2.2 并发编程中的两个关键问题

在并发编程中,需要处理两个关键问题:线程之间如何通信及线程之间如何同步(这里的线程是指并发执行的活动实体)。线程之间的通信机制有两种:共享内存和消息传递。

2.3 共享内存并发模型

在这种并发模型中,线程之间写-读内存中的公共状态进行隐式通信。例如:
int i = 1; //线程A执行
int b = i + 1; //线程B执行

  1. 线程A的这个写操作会将i这个共享变量的值首先写入A线程的本地内存中;然后再将i更新的值刷到主内存中。
  2. 线程B从主内存中读取共享变量i的值,写入到自己的本地内存中,然后进行下一步操作。
    这两步中线程A和线程B就实现了通过主内存之间的通信,其中本地内存只是个抽象概念,我们可以理解它是线程将共享变量写入主内存之前的一步缓存操作。Java并发采用的就是这种模型。

2.4 重排序

指令重排序的目的是为了提高程序执行性能。

重排序分3种类型。
1)编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重写安排语句的执行顺序。
2)指令级重排序。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
3)内存系统的重排序。由于处理器使用缓存和读/写缓存区,这使得加载和存储操作看上去可能是乱序执行。

编译器优化的重排序属于编译器重排序,指令级重排序和内存系统重排序属于处理器重排序。对于编译器层面的重排序,Java内存模型(JMM)会禁止一些特定类型的编译器重排序。

2.4.1 编译器排序

例如:
int a = 1; //1
int b = 2; //2
int c = a + b; // 3
编译器允许这三条语句的1和2重排序,不允许3跟1和2重排序,因为3操作需要1和2声明的变量。(这也是下面会讲到的 as-if-serial规则)

2.4.2 处理器重排序

对于处理器的重排序,通过插入特定类型的内存屏障指令来禁止特定类型的处理器重排序。例如:
处理器A:
a = 1; //A1
x = b; //A2
处理器B:
b = 2; //B1
y = a; //B2
处理器内存重排序导致了A2先于A1执行,这时处理器A会向主内存中获取到变量b的初始值0,而没有等到处理器B将b的值刷新到主内存中去。同理B处理器也会发生类似的内存操作重排序。所以最终结果导致了 x = y = 0;

2.5 happens-before简介

在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens -before关系。这里提到的两个操作既可以在一个线程之内,也可以事在不同线程之间。

对于并发编程中,我们较为关注的就是共享资源的写操作与读操作这两种操作,如果这两种操作有了happens-before关系,那么线程安全问题就变得简单得多。另外还有一点需要注意的是:如果操作A happens-before操作B,并不代表操作A一定要在操作B之前执行。(其实本人对这句话理解的也不是很透彻)

与程序员密切相关的happens-before规则如下。
程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
监视器锁规则:对于一个锁的解锁,happens-before于随后对这个锁的加锁。
volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
传递性:如果Ahappens-beforeB,且Bhappens-beforeC,那么Ahappens-beforeC。

2.6数据依赖性

数据依赖有3种类型:1. 写后读;2. 写后写;3.读后写。这三种类型中只要发生了重排序,执行结果就会发送改变。例如写后读:
a = 1; //1
b = a; //2
如果1和2重排序,就会导致b的结果发生变化。所以对于在单线程或者单个处理中,编译器的这种重排序是被禁止的。但是到了多处理器和多线程环境中,这三种数据依赖性不会被考虑的。所以编译器和处理器的重排序是导致线程安全问题的一个原因也是解决这类问题的一个切入点。

2.7 as-if-serial语义

不管怎么重排序,(单线程)程序的执行结果不能被改变。

这里的重点是在单线程程序中,存在数据依赖的操作不会被重排序,最常见的就是写后读这种数据依赖不会被重排序。as-if-serial语义保证了单线程程序一定会允许出预期的结果,保证了排序不会影响程序的结果。

在计算机中,软件技术和硬件技术有一个共同的目标:在不改变程序执行结果的前提下,尽可能提高并行度。

2.8 重排序对多线程的影响

讲到这,就进入了关键问题:线程安全问题。由于重排序的存在,就会导致在多线程程序中运行的结果和预期的不一致,从而导致了线程不同步问题。我么来看下从《Java并发编程的艺术》一书的一段代码

public class ReorderExample {
    int a = 0;
    boolean flag = false;
    public void writer(){
        a = 1;  //1
        flag = true;    //2
    }
    public void reader(){
        if (flag){  //3
            int i = a * a; //4
        }
    }
}

这段代码,如果线程A执行了writer()方法,线程B执行reader()方法。

  1. 操作1和操作2的重排序
    由于操作1和操作2之间没有数据依赖关系,在线程A这个单线程环境下,1和2会发生重排序,结果是2先一步执行,在同一时间上线程B在执行3操作,这时对于B来说主内存中a的值依然是0,flag已经变成了true,所以最终导致i = 0;
  2. 操作3和操作4的重排序
    我们假设1和2未发生重排序,在执行1时与此同时执行了操作4;这里需要说明的一点是虽然操作3和4存在控制关系,但是编译器处理器会采用猜测执行的手段来提高并行度。也就是操作4实际上是可以先于3执行的,这时i = 1。最后只有当操作3条件为真的时候,才会把计算结果写入变量i中。
    从上述的例子可以看出重排序破坏了多线程的语义,改变了程序的执行结果。

3 总结

上文讲到一下几个点:

  1. 并发编程的两个关键问题
  2. Java内存模型中的重排序
  3. Java内存模型中的happens-before和as-if-serial语义
  4. 多线程中的重排序以及导致的后果
    下一篇文章我们将会总结一种理想的内存模型:顺序一致性模型以及JMM如何保证程序上的顺序一致性;还会总结volatile的内存语义(重点)
    **

注:本文的引用和代码部分均出自于《Java并发编程的艺术》第三章。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
【1】项目代码完整且功能都验证ok,确保稳定可靠运行后才上传。欢迎下载使用!在使用过程,如有问题或建议,请及时私信沟通,帮助解答。 【2】项目主要针对各个计算机相关专业,包括计科、信息安全、数据科学与大数据技术、人工智能、通信、物联网等领域的在校学生、专业教师或企业员工使用。 【3】项目具有较高的学习借鉴价值,不仅适用于小白学习入门进阶。也可作为毕设项目、课程设计、大作业、初期项目立项演示等。 【4】如果基础还行,或热爱钻研,可基于此项目进行二次开发,DIY其他不同功能,欢迎交流学习。 【注意】 项目下载解压后,项目名字和项目路径不要用文,否则可能会出现解析不了的错误,建议解压重命名为英文名字后再运行!有问题私信沟通,祝顺利! 基于C语言实现智能决策的人机跳棋对战系统源码+报告+详细说明.zip基于C语言实现智能决策的人机跳棋对战系统源码+报告+详细说明.zip基于C语言实现智能决策的人机跳棋对战系统源码+报告+详细说明.zip基于C语言实现智能决策的人机跳棋对战系统源码+报告+详细说明.zip基于C语言实现智能决策的人机跳棋对战系统源码+报告+详细说明.zip基于C语言实现智能决策的人机跳棋对战系统源码+报告+详细说明.zip基于C语言实现智能决策的人机跳棋对战系统源码+报告+详细说明.zip基于C语言实现智能决策的人机跳棋对战系统源码+报告+详细说明.zip基于C语言实现智能决策的人机跳棋对战系统源码+报告+详细说明.zip基于C语言实现智能决策的人机跳棋对战系统源码+报告+详细说明.zip基于C语言实现智能决策的人机跳棋对战系统源码+报告+详细说明.zip基于C语言实现智能决策的人机跳棋对战系统源码+报告+详细说明.zip基于C语言实现智能决策的人机跳棋对战系统源码+报告+详细说明.zip
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值