第3章,Java内存模型

第3章,Java内存模型

并发编程模型的两个关键问题:
1、线程之间如何通信。
2、线程之间如何同步。

命令式编程中,线程之间的通信机制有两种:
1、共享内存。
2、消息传递。

Java的并发采用的是
JMM:Java内存模型,描述主内存与本地内存(线程持有的变量)之间的关系。

常见的内存处理器模型比JMM要弱。

JMM屏蔽了不同处理器内存模型的差异,他在不同的处理器平台之上为Java程序员呈现了一个一致的内存模型。

JMM是一个语言级的内存模型,处理器内存模型是一个硬件级的内存模型,顺序一致性内存模型是一个理论参考模型。

线程之间通信的机制:消息传递(显示通信)和共享内存(写读内存中的公共状态隐式通信)。
消息传递的同步是隐式进行的,共享内存的同步是显示进行的。
Java的并发采用的是共享内存的方式,同步是显示进行的,线程之间的通信是隐式进行的。

重排序

为了优化程序性能,编译器与处理器会对指令进行重排序。编译器与处理器不会对存在数据依赖的操作进行重排序。这种依赖仅针对在单处理器和单线程上执行的操作。在多处理器和多线程上执行的操作不被编译器和处理器考虑。
重排序分为三种类型:
1、编译器优化的重排序。
2、指令级并行的重排序。
3、内存系统的重排序。

在这里插入图片描述

第一步属于编辑器级别的重排序

第二步与第三步的重排序属于处理器级别的重排序。

JMM会限制一些编译器级别的重排序,通过插入内存屏障来限制处理器级别的重排序。

JMM属于语言级别的内存模型,通过禁止特定类型的编译器级别重排序以及处理器级别的重排序,为程序员提供一致的内存可见性保证。

在这里插入图片描述

as-if-serial

不管怎么重排序,单线程程序的执行结果不能改变。此原则不必担心重排序,也不必担心内存可见性问题。

happens-before

在JMM中,如果一个操作要对另一个操作可见,那么两个操作之间必须要存在happens-before关系,这种关系可以是在一个线程内,也可能在不同的线程之间。

happens-before是JMM中最重要的概念。

JMM的设计理念既要为程序员提供强的内存保证,又要放松对处理器和编译器的限制。

所以对于会改变结果的重排序,JMM要求处理器与编译器必须要限制。
对于不改变结果的重排序,JMM对处理器与编译器不做要求。

在这里插入图片描述

与程序员密切相关的happens-before规则有
1、程序顺序原则:一个线程中的每个操作的执行结果,happens-before于它后续的所有操作。
2、监视器锁原则:对一个锁的解锁,happens-before对这个锁的加锁。
3、volatile变量原则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
4、传递性:如果Ahappens-before与B,且Bhappens-before于C,那么Ahappens-before于C。
5、A线程调用ThreadB.start,优先于ThreadB中的所有操作。
6、A线程调用ThreadB.join,此句滞后于ThreadB中的所有操作。

在不改变程序执行结果的前提下,尽可能提供并行度。

JAVA内存模型的设计原理,处理器内存模型,顺序一致性内存模型的关系。

顺序一致性内存模型
顺序一致性内存模型是一个理论参考模型,处理器内存模型,JAVA内存模型都会以顺序一致性内存模型为参考。
顺序一致内存模型有两大特性:
1、一个线程中的所有操作必须按照程序的顺序来执行。
2、(不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。所有操作之前具有全序关系,同一时刻只能有一个线程操作他们的共享内存。
同步的了的
在这里插入图片描述
没有同步的
在这里插入图片描述

A和B看到的顺序都是B1-》A1-》A2-》B2-》A3-》B3,因为在顺序一致性内存模型中每个操作必须立即对任意线程可见。JMM中就没有这个保证,可能整体无序,也可能每个线程看到的顺序不一致。

数据通过总线在内存和处理器之间传递信息。同一时刻只能有一个处理器访问总线,总线的这种机制保证了所有处理器对内存的访问是串行的。这个特性确保了单个总线事务中的操作具有原子性。

写缓存区可以保证写内存的高效性,可以批量刷新,或者合并对内存的读写,但是写缓冲区仅仅对它所在的处理器可见。synchronized的内存语义。

volatile的内存语义

对volatile变量的单个读或者写,可以理解为使用同一个锁对这些单个读或者写操作做了同步。
volatile写和锁的释放有相同的内存语义,volatile的读和获取锁有相同的内存语义。
可见性:
1、锁的happens-before规则保证了释放锁和获得锁的两个线程之间的内存可见性,这意味着对一个volatile变量的读总是能看到任意线程对这个volatile变量的最后的写入。
原子性:
1、即使是64位long或者double变量,声明为volatile的,仍然可以保证其原子性。
2、如果是多个volatile操作或类似于volatile++这种符合操作,这些操作整体上不具有原子性。

volatile写-读建立的happens-before关系保证了:当A线程写了一个volatile变量后(实质上JMM会把线程对应的本地内存中的共享变量刷新到主内存中),B线程读同一个volatile变量(实质上JMM会把线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量)。A线程在写volatile变量之前所有可见的共享变量,在B线程读同一个volatile变量后,将立即变得对线程B可见。
对同一个volatile变量的写读,实质上是写线程通过主内存向读的进程发出消息

在这里插入图片描述

当第一个操作为volatile读的时候,不允许重排序。
当后续操作为volatile的写的时候,不允许重排序。
volatile写后volatile读,不允许重排序。

为了保障volatile的内存语义:
在volatile的写之前插入了storestore内存屏障。
在volatile的写之后插入了storeload内存屏障。
在volatile的读操作之后插入了loadload内存屏障。
在volatile的读操作之后插入了loadstore内存屏障。

锁(synchronized)的内存语义与volatile相同

锁可以让临界区互斥执行,还可以让释放锁的线程向获取同一个锁的线程发送消息。
当线程释放锁时,JMM会把线程对应的本地内存中的共享变量刷新到主内存中。
当线程获取锁时,JMM会把线程对应的本地内存置为无效。
底层依赖ReentrantLock(依赖Java同步器框架AbstractQuenedSynchronizer,简称AQS)实现,AOS使用一个整型的volatile变量(state)来维护同步状态。
ReentrantLock分为公平锁(通过volatile型变量state的内存语义实现,写线程内的所有共享变量将对另一个读volatile变量的线程可见)与非公平锁(CAS)。
公平锁与非公平锁的内存语义:
在释放锁的时候都会写一个volatile变量state。
公平锁在获取锁的第一个操作是读一个volatile型变量。
非公平锁获取锁是首先CAS更新一个volatile型变量state,此CAS操作同时具有volatile读和volatile写的内存语义。

Concurrent包,AQS,阻塞队列等的实现

因为CAS同时具有volatile内存读写的功能,所有线程之间的通信方式有以下四种:
1、A线程写volatile变量,随后B线程读这个volatile变量。
2、A线程写volatile变量,随后B线程用CAS更新这个volatile变量。
3、A线程用CAS更新这个volatile变量,随后B线程用CAS更新这个volatile变量。
4、A线程用CAS更新这个volatile变量,随后B线程读这个volatile变量。

final的内存语义

与锁和volatile的内存语义相比,final的内存语义更像是对普通变量的读和写。
对于final域,处理器和编译器需要遵守两个重排序规则:
1、在构造函数内,对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
2、初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
写final域的重排序规则:
1、禁止把final域的写重排序到构造函数之外。
2、编译器会在final域写之后,构造函数return之前,插入一个storestore屏障。
3、如果final域修饰的不是基本数据类型,是引用类型,那么读final域的写增加了如下约束,在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个改造对象的引用赋值给一个引用变量,这两个操作之间不可以重排序。
读final域的重排序规则:
1、在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作。
2、处理器会在读final域的前面插入一个loadload屏障。
总结:
final域的内存语义为了保证在构造函数中对final域的赋值,可以对其他线程可见。

双重检查锁定与延迟初始化

双重检查锁定不是正确的,第四行检查时,发现不是null,但是instance并没有初始化完毕,因为第七行的初始化可以分为三步。

在这里插入图片描述

既然知道了问题,那么解决的办法:
1、阻止步骤2和步骤3进行重排序。要初始化的类对象,声明为volatile的。
2、步骤2和步骤3的重排序,使用下面的原则来实现,调用需要延迟初始化的类的静态成员,将立即开始初始化。

根据Java语言规范,在首次发生下列任何一种情况时,一个类或者接口类型T将被立即初始化。

1、T是一个类,而且一个T类型的实例被创建。
2、T是一个类,而且T中声明的一个静态方法被调用。
3、T中声明的一个静态字段被赋值。
4、T中声明的一个静态字段被使用,而且这个字段不是一个常量字段。
5、T是一个顶级类,而且一个断言语句嵌套在T内部被执行。

Java语言规范规定,对于每一个类或接口C,都有一个唯一的初始化锁LC与之对应。

总结延迟初始化

如果确实需要对实例字段使用线程安全的延迟初始化,请使用基于volatile的延迟初始化方案。如果需要对静态字段使用线程安全的延迟初始化,请使用基于类初始化的方案。

所有实例域,静态域,和数组元素,存储在堆内存中,是线程之间共享的,存在内存可见性问题,是共享变量

局部变量,方法定义参数,和异常处理参数,不会在线程之间共享,不存在内存可见性问题。

线程之间的通信由JMM控制。JMM决定一个线程对共享变量修改时,何时对其他线程可见。

共享变量存储在主内存中,每个线程有一个自己的私有本地内存。本地内存是一个抽象概念,并不真实存在,它是共享内存的副本。

在这里插入图片描述

数据竞争的定义

1、一个线程中在写一个变量。
2、另一个线程中在读同一个变量。
3、而且写和读没有通过同步来排序。

发布了95 篇原创文章 · 获赞 32 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/jiangxiulilinux/article/details/104851919