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
    评论
C语言是一种广泛使用的编程语言,它具有高效、灵活、可移植性强等特点,被广泛应用于操作系统、嵌入式系统、数据库、编译器等领域的开发。C语言的基本语法包括变量、数据类型、运算符、控制结构(如if语句、循环语句等)、函数、指针等。在编写C程序时,需要注意变量的声明和定义、指针的使用、内存的分配与释放等问题。C语言常用的数据结构包括: 1. 数组:一种存储同类型数据的结构,可以进行索引访问和修改。 2. 链表:一种存储不同类型数据的结构,每个节点包含数据和指向下一个节点的指针。 3. 栈:一种后进先出(LIFO)的数据结构,可以通过压入(push)和弹出(pop)操作进行数据的存储和取出。 4. 队列:一种先进先出(FIFO)的数据结构,可以通过入队(enqueue)和出队(dequeue)操作进行数据的存储和取出。 5. 树:一种存储具有父子关系的数据结构,可以通过序遍历、前序遍历和后序遍历等方式进行数据的访问和修改。 6. 图:一种存储具有节点和边关系的数据结构,可以通过广度优先搜索、深度优先搜索等方式进行数据的访问和修改。 这些数据结构在C语言都有相应的实现方式,可以应用于各种不同的场景。C语言的各种数据结构都有其优缺点,下面列举一些常见的数据结构的优缺点: 数组: 优点:访问和修改元素的速度非常快,适用于需要频繁读取和修改数据的场合。 缺点:数组的长度是固定的,不适合存储大小不固定的动态数据,另外数组在内存是连续分配的,当数组较大时可能会导致内存碎片化。 链表: 优点:可以方便地插入和删除元素,适用于需要频繁插入和删除数据的场合。 缺点:访问和修改元素的速度相对较慢,因为需要遍历链表找到指定的节点。 栈: 优点:后进先出(LIFO)的特性使得栈在处理递归和括号匹配等问题时非常方便。 缺点:栈的空间有限,当数据量较大时可能会导致栈溢出。 队列: 优点:先进先出(FIFO)的特性使得

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值