一、Java内存模型介绍

内存模型的作用范围:

在Java中,所有实例域、静态域和数组元素存放在堆内存中,线程之间共享,下文称之为“共享变量”。局部变量、方法参数、异常处理器等不会在线程之间共享,不存在内存可见性问题,也不受内存模型的影响。

重排序与可见性:

现代编译器在编译源码时会做一些优化处理,对代码指令进行重排序;现代流水线结构的处理器为了提高并行度,在执行时也可能对指令做一些顺序上的调整。重排序包括编译器重排序、指令级并行重排序和内存系统重排序等。一般来说,编译器和处理器在做重排序的时候都会做一些保证,保证程序的执行结果与重排序之前指令的执行结果相同。即as-if-serial,不管怎样重排序,都不能改变程序的执行结果。

CPU在执行指令时一般都会使用缓存技术来提高效率,如果不同线程使用不同的缓存空间则会造成一个线程对一个共享变量的更新不能及时反映给其他线程,也就是多线程对共享变量更新的可见性问题,这个问题是非常复杂的。

Java内存模型的抽象:

对于上述问题,Java内存模型(JMM)为程序员提供了一个抽象层面的描述,我们不用去关心编译器、处理器对指令做了怎样的重排序,也不用关心复杂的系统缓存机制,只要遵循JMM的规则,JMM就能为我们提供代码顺序性、共享变量可见性的保证,从而得到预期的执行结果。

JMM决定了一个线程对共享变量的写入何时对另一个线程可见。从抽象来讲,线程共享变量存放在主内存(main memory),每个线程持有一个本地内存(local memory),本地内存中存储了该线程读写共享变量的副本(本地内存是JMM的一个抽象概念,并不是真实存在的)。如下图:

如果A、B两个线程要通信要经过以下两步:首先线程A将本地内存中更新过的共享变量刷新到主内存中,然后线程B到主内存中读取A之前更新过的变量。

JMM通过控制主内存与每个线程的本地内存之间的交互来为Java程序员提供可见性保证。

重排序:

现代编译器和处理器会对指令执行的顺序进行重排序,以此提高程序的性能。这些重排序可能会导致多线程程序出现内存可见性问题。为了不改变程序的执行结果,对于编译器,JMM会禁止特定类型的编译器重排序;对于处理器重排序,JMM要求在Java编译生产指令序列时,插入特定类型的内存屏障(memory barriers)来禁止特定类型的重排序。

JMM把内存屏障分为以下四类:

屏障类型 指令示例 说明
LoadLoad Barriers Load1; LoadLoad; Load2 确保Load1数据的装载之前于在Load2及其所有后续装载指令
StoreStore Barriers Store1; StoreStore; Store2 确保Store1刷新数据到内存之前与Store2及其后续存储指令
LoadStore Barriers Load1; LoadStore; Store2 确保Load1数据装载之前于Store2及其后续存储指令
StoreLoad Barriers Store1; StoreLoad; Load2

确保Store1刷新数据到内存之前于Load2及其后续装载指令。

StoreLoad Barriers会使该屏障之前的所有内存访问指令完成后才执行屏障后的指令。

StoreLoad Barriers是一个全能型屏障,同时具有其他三个屏障的效果。

Happens-before:

从JDK1.5开始,Java使用新的JSR-133内存模型(以下所有都是针对该内存模型讲的),使用happens-before的概念来阐述操作之间的内存可见性。

如果一个操作要对另一个操作可见,那这两个操作之间必须存在happens-before关系。这两个操作可以在一个线程内,也可以在不同线程之间。ps.(两个操作存在happens-before关系并不意味着前一个操作必须在后一个操作之前执行,仅仅要求前一个操作对后一个操作可见。)

常见的与程序员相关的happens-before规则如下:

①程序顺序规则:一个线程中的每个操作happens-before于其后的任意操作;

②监视器锁规则:对一个监视器的解锁happens-before于随后对这个监视器的加锁;

③volatile规则:对一个volatile域的写happens-before于任意后续对该域的读操作(该规则多个线程之间也成立);

④传递性:如果A happens-before B,且B happens-before C,那么A happens-before C

数据依赖性:如果两个操作访问同一个变量,且这两个操作其中一个为写操作时,这两个操作就存在数据依赖性。如下示例:

写后读

a=1;

b=a;

写后写

a=1;

a=2;

读后写

a=b;

b=a;

上述三类情况存在数据依赖性,此时不允许重排序,否则程序的结果可能会改变。

as-if-serial语义:

as-if-serial语义的意思是:在单线程内,不管怎么重排序,程序的执行结果不变,在程序员看来,就像顺序执行的一样。

示例:

 
a = ; //A
b = ; //B
c = a + b; //C

前两条语句就可以进行重排序,而第三条语句与前两条存在依赖关系,不能重排序。

上述A happens-before B,B happens-before C,但并不保证A在B之前执行,只需要保证操作A对B可见(这里A操作不需要对B可见,因此可以重排)

重排序对多线程的影响:

示例:

 
class Demo {
    boolean flag = false;
    int a = ;
   
    public void fun1() {
        a = ; //A
        flag = true; //B
    }
   
    public void fun2() {
        if (flag) { //C
            a = a + a; //D
        }
    }
}

假设上述类中fun1()和fun2()在不同线程中执行,操作A、B没有依赖关系,可能被重排序;操作C、D虽然存在控制依赖关系,现代编译器和处理器为了提高并行度,可能采取激进的方法(即先求出if语句块中的值存于临时变量中,如果if条件为真则使用该值,否则丢弃)对其进行重排序,这都可能改变程序的执行结果。

顺序一致性内存模型:

计算机科学家们提出了一个理想化的理论参考模型--顺序一致性模型,它为程序员提供了极强的内存可见性,具有如下两大特性:

①一个线程中的所有操作必须按照程序顺序来执行;

②所有线程(无论同步与否)都只能看到一个单一的操作执行顺序。每个操作都必须是原子的且立刻对所有线程可见。

示例:

假设有A和B两个线程并发执行,A线程中有三个操作,顺序是A1->A2->A3,线程B中也有三个操作,顺序是B1->B2->B3。 先假设这两个线程使用监视器同步,A线程先获得监视器,执行完毕释放监视器后线程B开始执行。那么他们在顺序一致性模型中执行效果如下:

现在我们再假设这两个线程未进行同步,其在顺序一致性模型中执行效果如下:

可以看到,未同步的程序在顺序一致性模型中虽然整体执行顺序是无序的,但所有线程都只看到一个一致的整体执行顺序。如上图,线程A和B看到的执行顺序都是B1->A1->A2->B2-A3->B3。之所以能得到这个保证是因为顺序一致性内存模型中的每个操作必须立即对任何线程可见。

但是JMM中没有这个保证。比如当前线程写数据到本地内存中,在还没有刷新到主内存之前,这个写操作只对当前线程可见,从其他线程角度观察,可以认为这个写操作根本还没有被当前线程执行过。这种情况下,当前线程和其他线程看到的操作执行顺序将不一致。

同步程序的一致性效果:

示例:

 
class SynchronizedDemo {
    int a = ;
    boolean flag = false;
    public synchronized void write() {
        a = ;
        flag = true;
    }

public synchronized void read() {
        if(flag) {
            int i = a;
        }
    }
}

上述代码使用同步方法,线程A先执行write()方法,释放锁后线程B获取锁并执行read()方法,执行流程如下:

在顺序一致性模型中,所有操作按顺序执行。在JMM中,临界区内的代码可以重排序(JMM不允许临界区内的代码“逸出”到临界区之外),JMM会在进入和退出临界区的关键点上做一些限定,使得现场在这两个关键点处具有和顺序一致性模型具有相同的内存视图。虽然现场A在临界区内做了重排序,但由于监视器的互斥性,这里线程B根本无法“观察”到线程A在临界区内的重排序,这样既提高了效率又不改变程序的执行结果。

对于未同步的多线程程序,JMM只提供最小安全性:线程执行读操作取得的值,要么是之前线程写入的,要么是默认值(0,null,false),JMM保证线程读取的数据不是无中生有冒出来的。为了实现最小安全,JVM在堆上分配对象时首先会清空内存空间,然后才分配对象(因此对象分配时,域的默认初始化已经完成)。

此外,JMM的最小安全不保证对64位的long和double型变量的读写具有原子性,而顺序一致性模型保证对所有内存读写操作具有原子性。

二、Volatile特性

volatile变量的单次读写,相当于使用了一个锁对这些单个读/写做了同步。

原子性:对volatile变量的单次读写操作具有原子性(ps.这里存在争议,暂且这么写,保留意见);

可见性:锁的happens-before规则保证释放锁和获取锁的两个线程之间的可见性,这意味着对一个volatile变量的读操作总能看到之前任意线程对这个volatile变量最后的写入,即对volatile变量的写操作对其他线程立即可见。

当写一个volatile变量时,JMM会把线程对应的本地内存中的共享变量刷新到主内存;

当读一个volatile变量时,JMM会把改下昵称对应的本地内存置为无效,接下来从主内存中读取共享变量的值。

从内存语义的角度来说,volatile的写-读于锁的释放-获取具有相同的内存效果。因此如果线程A对volatile变量的写操作在线程B对volatile变量的读操作之前,则其存在happens-before关系。

示例:

class VolatileDemo {
volatile boolean flag = false;
int a=0;
public void fun1() {
a=1; //A
flag = true; //B
} public void fun2() {
if (flag) { //C
a=a+a; //D
}
}
}

上述操作A happens-before 操作B,操作C happens-before 操作D,如果线程1调用fun1()方法之后线程2调用fun2()方法,则操作B happens-before 操作C,根据happens-before的传递性,则有A happens-before D,因此可以保证操作D可以正确读取到操作A的赋值。

Volatile的内存语义是JMM通过在volatile读写操作前后插入内存屏障实现的。

三、锁的特性

锁的释放与获取遵循happens-before规则,释放锁线程临界区的操作结果对获取锁的线程可见。

当线程释放锁时,JMM会把线程对应的本地内存中的共享变量刷新到主内存;

当线程获取锁时,JMM会把改下昵称对应的本地内存置为无效,接下来从主内存中读取共享变量的值。

ReentrantLock是java.util.concurrent.locks包下的一个锁的实现,依赖对volatile变量的读写和compareAndSet(CAS)操作实现锁机制。其中CAS操作使用不同的CPU指令实现单次操作的原子性,具有volatile读写操作相同的内存语义。 类图如下:

ReentrantLock根据对抢占锁的线程的处理方式不同,分为公平锁和非公平锁,首先看公平锁,使用公平锁加锁时,加锁方法lock()的方法调用主要有以下四步:

1. ReentrantLock : lock()
2. FairSync : lock()
3. AbstractQueuedSynchronizer : acquire(int arg)
4. ReentrantLock : tryAcquire(int acquires)

在第四步才开始真正加锁,该方法的源码如下:

protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();();//获取锁的开始,state是volatile类型变量
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}

从上面方法可以看出,加锁方法首先读取volatile变量state。

使用公平锁的unlock()方法调用轨迹如下:

1. ReentrantLock : unlock()
2. AbstractQueuedSynchronizer : release(int arg)
3. Sync : tryRelease(int releases)

在第三步调用时才真正开始释放锁,该方法源码如下:

protected final boolean tryRelease (int releases){
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false ;
if (c == 0){
free = true ;
setExclusiveOwnerThread( null );
}
setState(c);//释放锁后,写volatile变量state
return free;
}

从上面代码可以看出,在释放锁的最后写volatile变量state。

公平锁在释放锁的最后写volatile变量state,在获取锁的时候首先读这个volatile变量。根据volatile的happens-before规则,释放锁的线程在写volatile变量之后该变量对获取锁的线程可见。

Java中的CAS操作同时具有volatile读和volatile写的内存语义,因此Java线程之间通信现在有了以下四种方式:

1、A线程写volatile变量,随后B线程读这个volatile变量。

2、A线程写volatile变量,随后B线程用CAS更新这个volatile变量。

3、A线程用CAS更新一个volatile变量,随后B线程用CAS更新这个volatile变量。

4、A线程用CAS更新一个volatile变量,随后B线程读这个volatile变量。A线程写

Java的CAS会使用现代处理器上提供的高效机器级别原子指令,这些原子指令以原子方式对内存执行读-改-写操作,同时,volatile变量的读/写和CAS可以实现线程之间的通信。把这些特性整合在一起,就形成了整个concurrent包得以实现的基石。如果我们仔细分析concurrent包的源代码实现,会发现一个通用化的实现模式:

1、首先,声明共享变量为volatile;

2、然后,使用CAS的原子条件更新来实现线程之间的同步;

3、同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。

四、Final 的特性

与前面介绍的锁和volatile相比较,对final域的读和写更像是普通的变量访问。对于final域,编译器和处理器要遵守两个(分别对应读写)重排序规则:

1、在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

2、初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。

写final域的重排序规则

写final域的重排序规则禁止把final域的写重排序到构造函数之外。这个规则的实现包含下面2个方面:

1、JMM禁止编译器把final域的写重排序到构造函数之外。

2、编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障。这个屏障禁止处理器把final域的写重排序到构造函数之外 。

写final域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域不具有这个保障

对于引用类型,写final域的重排序规则对编译器和处理器增加了如下约束:

在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。

读final域的重排序规则

在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作(注意,这个规则仅仅针对处理器)。编译器会在读final域操作的前面插入一个LoadLoad屏障。初次读对象引用与初次读该对象包含的final域,这两个操作之间存在间接依赖关系。由于编译器遵守间接依赖关系,因此编译器不会重排序这两个操作。

读final域的重排序规则可以确保:在读一个对象的final域之前,一定会先读包含这个final域的对象的引用。在这个示例程序中,如果该引用不为null,那么引用对象的final域一定已经被A线程初始化过了。

Java内存模型知识点小结---《深入理解Java内存模型》(程晓明)读书总结的更多相关文章

  1. Java虚拟机内存溢出异常--《深入理解Java虚拟机》学习笔记及个人理解(三)

    Java虚拟机内存溢出异常--<深入理解Java虚拟机>学习笔记及个人理解(三) 书上P39 1. 堆内存溢出 不断地创建对象, 而且保证创建的这些对象不会被回收即可(让GC Root可达 ...

  2. Java四种引用--《深入理解Java虚拟机》学习笔记及个人理解(四)

    Java四种引用--<深入理解Java虚拟机>学习笔记及个人理解(四) 书上P65. StrongReference(强引用) 类似Object obj = new Object() 这类 ...

  3. Java基础系列1:深入理解Java数据类型

    Java基础系列1:深入理解Java数据类型 当初学习计算机的时候,教科书中对程序的定义是:程序=数据结构+算法,Java基础系列第一篇就聊聊Java中的数据类型. 本篇聊Java数据类型主要包括四个 ...

  4. Java进阶(三十六)深入理解Java的接口和抽象类

    Java进阶(三十六)深入理解Java的接口和抽象类 前言 对于面向对象编程来说,抽象是它的一大特征之一.在Java中,可以通过两种形式来体现OOP的抽象:接口和抽象类.这两者有太多相似的地方,又有太 ...

  5. Java内存模型解惑--观深入理解Java内存模型系列文章有感(二)

    1.volatile关键字修饰的域的特性 当我们声明共享变量为volatile后,对这个变量的读/写将会很特别.理解volatile特性的一个好方法是:把对volatile变量的单个读/写,看成是使用 ...

  6. Java并发指南2:深入理解Java内存模型JMM

    本文转载自互联网,侵删   一:JMM基础与happens-before 并发编程模型的分类 在并发编程中,我们需要处理两个关键问题:线程之间如何通信及线程之间如何同步(这里的线程是指并发执行的活动实 ...

  7. 夯实Java基础系列10:深入理解Java中的异常体系

    目录 为什么要使用异常 异常基本定义 异常体系 初识异常 异常和错误 异常的处理方式 "不负责任"的throws 纠结的finally throw : JRE也使用的关键字 异常调 ...

  8. 夯实Java基础系列11:深入理解Java中的回调机制

    目录 模块间的调用 多线程中的"回调" Java回调机制实战 实例一 : 同步调用 实例二:由浅入深 实例三:Tom做题 参考文章 微信公众号 Java技术江湖 个人公众号:黄小斜 ...

  9. 夯实Java基础系列13:深入理解Java中的泛型

    目录 泛型概述 一个栗子 特性 泛型的使用方式 泛型类 泛型接口 泛型通配符 泛型方法 泛型方法的基本用法 类中的泛型方法 泛型方法与可变参数 静态方法与泛型 泛型方法总结 泛型上下边界 泛型常见面试 ...

随机推荐

  1. 使用Advanced Installer制作IIS安装包(一:配置IIS和Web.config)

    前言:大过年的,写篇文章不容易,还是给自己点个赞~~年前找了下.net安装包的制作方法,发现Visual Studio自带的制作工具使用起来非常麻烦,需要单独下载安装包,并且什么激活认证等等屁事相当麻 ...

  2. 第53章 结束会话端点(End Session Endpoint) - Identity Server 4 中文文档(v1.0.0)

    结束会话端点可用于触发单点注销(请参阅规范). 要使用结束会话端点,客户端应用程序会将用户的浏览器重定向到结束会话URL.用户在会话期间通过浏览器登录的所有应用程序都可以参与注销. 注意 终端会话端点 ...

  3. 第26章 联合注销 - Identity Server 4 中文文档(v1.0.0)

    联合注销是指用户使用外部身份提供程序登录IdentityServer,然后用户通过IdentityServer未知的工作流程注销该外部身份提供程序的情况.当用户注销时,对IdentityServer进 ...

  4. [转]Chrome 错误代码:ERR_UNSAFE_PORT

    本文转自:https://blog.csdn.net/testcs_dn/article/details/39186225 最近在用Nginx发布多个站点测试,使用了87.88端口, 88端口访问正常 ...

  5. C# string数组转int数组(转载)

    C# string数组转int数组   用法 1 2 3 4 5 6 7 8 9 10 11 12 13 14 //字符串数组(源数组) string[] sNums = new[] {"1 ...

  6. 如何保证MongoDB的安全性?

    上周写了个简短的新闻<MongoDB裸奔,2亿国人求职简历泄漏!>: 根据安全站点HackenProof的报告,由于MongoDB数据库没有采取任何安全保护措施,导致共计202,730,4 ...

  7. Design Mobile实现国际化

    参考:https://mobile.ant.design/components/locale-provider-cn/   

  8. Js与jQuery的相互转换

    $()与jQuery() jQuery中$函数,根据传入参数的不同,进行不同的调用,实现不同的功能.返回的是jQuery对象 jQuery这个js库,除了$之外,还提供了另外一个函数:jQuery j ...

  9. z-tree 回显所有选中的id

    //回显选择的checkbox函数 function treeHxIdFun(obj) { var objTree = $.fn.zTree.init($("#demo"), se ...

  10. python 图片在线转字符画预览

    文章链接:https://mp.weixin.qq.com/s/yiFOmljhyalE8ssAgwo6Jw 关于python图片转字符画,相信大家都不陌生,经常出现在 n个超有趣的python项目中 ...