1 CPU中的三级缓存及可见性问题

  1.1 简介

  1.2 缓存行Cacheline

  1.3 可见性问题-缓存一致性协议

2 JAVA中的有序性问题

  2.1 指令重排简介

  2.2 as-if-serial语义

    2.2.1 数据依赖

    2.2.2 控制依赖

  2.3 指令重排示例

    2.3.1 代码

    2.3.2 代码分析

    2.3.3 执行结果

3 this逸出问题

  3.1 一个对象的创建指令

  3.2 实例代码

4 DCL(Double Check Lock)

  4.1 单例模式Double Check Lock

  4.2 更安全的单例-静态内部类单例模式

5  JAVA怎么解决可见性和有序性的问题

  5.1 简介

  5.2 内存屏障(Memory Barrier)

  5.3 JVM屏障类型

  5.4Volatile修饰的变量

  5.5 屏障的底层实现

JAVA虚拟机22-原子性、可见性与有序性、先行发生原则:https://www.cnblogs.com/jthr/p/15780760.html

1 CPU中的三级缓存及可见性问题

1.1 简介

  ALU访问Registers里面的数据的速度是ALU访问内存数据的100倍

  为了提高访问效率,在CPU和内存之间有了多级缓存,不用每次都去访问内存里面的数据,缓存里面有直接访问缓存就可以了

  现在计算机大多是三级缓存,如下图

计算机上有两个双核CPU,L1,L2,L3就是三级缓存,L1和L2在核内,L3在核外

当核1需要使用内存里面的一个x,会先看L1有没有,没有就看L2,再没有就看L3,还是没有,去内存中取,存入到L3,L2,L1中。对x进行修改了,只需要修改离它最近的一个缓存,其他几个缓存中的值不修改

1.2 缓存行Cacheline

而且它去内存中拿数据不是一个一个的拿,是一块一块的拿放入缓存中(提高缓存命中率)。现在计算机中,会取一个Cacheline(缓存行)的数据,缓存行大小默认是64kb

1.3 可见性问题-缓存一致性协议

  如下图,现在有一个缓存行的数据,里面有x和y。左侧CPU需要用到x,右侧CPU需要用到y,它们都会把这个缓存行读取到自己的缓存里面。那么当左侧对x修改或者右侧对y修改时,是需要通知对方这个数据修改了的,因为它们使用的同一块缓存行数据数据。这个通知机制叫做缓存一致性协议,它是硬件级别的协议,软件控制不了的,所有的CPU都有这种机制

2 JAVA中的有序性问题

2.1 指令重排简介

  在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分3种类型:

    1)编译器优化的重排序。编译器在不改变单线程程序语义(单线程执行结果一致)的前提下,可以重新安排语句的执行顺序。

    2)指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism,ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

    3)内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

  1属于编译器重排序,2和3属于处理器重排序。从Java源代码到最终实际执行的指令序列,会分别经历下面3种重排序:

2.2 as-if-serial语义

  不管指令怎么重排序,在单线程下执行结果不能被改变。不管是编译器级别还是处理器级别的重排序都必须遵循as-if-serial语义。

  为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序。但是as-if-serial规则允许对有控制依赖关系的指令做重排序,因为在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果,但是多线程下确有可能会改变结果。

2.2.1 数据依赖

int a = 1; // 1
int b = 2; // 2
int c = a + b; // 3

上述代码,ab不存在依赖关系,所以1、2可以进行重排序;c依赖 ab ,所以3必须在1、2的后面执行

2.2.2 控制依赖

public void use(boolean flag, int a, int b) {
if (flag) { // 1
int i = a * b; // 2
}
}

flagi存在控制依赖关系。当指令重排序后,2这一步会将结果值写入重排序缓冲(Reorder Buffer,ROB)的硬件缓存中,当判断为true时,再把结果值写入变量i中

2.3 指令重排示例

2.3.1 代码
public class LuanXuTest {

    public static int x,y,a,b;
public static void main(String[] args) throws InterruptedException { for (long i = 0;i < Long.MAX_VALUE;i++){
CountDownLatch latch = new CountDownLatch(2);
x = 0;
y = 0;
a = 0;
b = 0;
Thread t1 = new Thread(()->{
a = 1;
x = b;
latch.countDown();
}); Thread t2 =new Thread(()->{
b = 1;
y = a;
latch.countDown();
}); t1.start();
t2.start();
latch.await();
if(x == 0 && y == 0){
System.out.println("第" + i + "次执行");
break;
}
}
}
}

2.3.2 代码分析

  如果指令不重排,按照顺序执行,那么只会有三种执行结果

1)x=0,y=1

2)x=1,y=2

3)x=1,y=0

 2.3.3 执行结果

第1603次执行

发现出现了x=0,y=0的情况

只有在以下两种指令重排的情况下才会出现x=0,y=0。说明指令确实重排了

3 this逸出问题

  this逃逸是指在构造函数返回之前其他线程就持有该对象的引用。调用尚未构造完成的对象的方法可能引起奇怪的问题

3.1 一个对象的创建指令

public class T {

    public static void main(String[] args) {
Object a = new Object();
} }

我们通过Jclasslib(直接在idea插件里面去搜索安装即可)插件来查看它的字指令

从指令来看,Object a = new Object()这一个操作不是原子性的,它主要分为三步(3dup指令这里不管)

1)new 分配内存创建对象

2)invokespecial 初始化(包含赋值操作)

3)astore_1关联,把User关联到a

由于这一个操作分为几步来走,经过指令重排,

如果发生指令重排,就可能发生下面情况

3.2 实例代码

  在LuanxuThisTest的构造方法中,启动了一个线程去获取当前对象的number,就可能存在获取到的number是默认值0而不是8。

  因为对象LuanxuThisTest的实例创建分为三步,第一步分配内存创建了对象,变量此时是默认值,而还没有完成初始化赋值,但是由于在构造方法中启动了一个线程,这个线程也在执行,它可能在创建LuanxuThisTest的第二步完成前就去获取到了LuanxuThisTes对象,而此时的LuanxuThisTest对象只是个半成品,它的number属性还是默认值0

  当然,下面代码执行很小几率才能获取到0

public class LuanxuThisTest {

    private int number = 8;

    public LuanxuThisTest(){
new Thread(()->{
System.out.println(this.number);
}).start();
} public static void main(String[] args) throws IOException {
while (true){
new LuanxuThisTest();
} }
}

4 DCL(Double Check Lock)

4.1 单例模式Double Check Lock

下面单例模式使用就是Double Check Lock

public class Girlfiriend extends Friend{
 private int number = 10;
private static final Girlfiriend gf;
private Girlfiriend (){
}
public static Girlfiriend getGirlfriend(){
if(gf==null){
synchronized(Girlfriend.class){
if(gf==null){
gf= new Girlfriend();
}
}
}
return gf;
  }
}
  在多线程情况下,当第一个线程在new Girlfriend()的过程中,还没有完成初始化赋值,但是已经完成关联了(发生了指令重排),另一个线程执行就第一个if(gf==null)的时候,由于已经完成关联,。所以这里if判断为false,会把没有构造完全的对象返回给第二个线程。所以在这里,可能导致返回不正确的结果
  补充一下:synchronized可以保证可见性和原子性,但是不能保证有序性。也就是说synchronized里面的也可能发生指令重排
  可以使用volatile修饰这个变量来保证有序性。
 
4.2 更安全的单例-静态内部类单例模式
public class Girlfiriend extends Friend{
 private int number = 10;
private Girlfiriend (){
}
public static Girlfiriend getGirlfriend(){
return GirlfriendMother.gf;
}
private static class GirlfriendMother{
private static final Girlfiriend gf= new Girlfiriend ();
}
}
  第一次加载Girlfriend的时候,gf不会被初始化,(java类只有被调用的时候才会初始化)。这种方式不仅能够确保线程的安全,也能够保证单例对象的唯一性,同时也延迟了单例的实例化。
 
5  JAVA怎么解决可见性和有序性的问题
5.1 简介
  想要让两条指令不重排,只需要在它们中间加一个屏障。
 

5.2 内存屏障(Memory Barrier)

  内存屏障又称内存栅栏,是一个CPU指令,它的作用有两个:

  1)保证特定操作的顺序

  2)保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)

  由于编译器和处理器都能执行指令重排的优化,如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说 通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化。 内存屏障另外一个作用是刷新出各种CPU的缓存数,因此任何CPU上的线程都能读取到这些数据的最新版本。

5.3 JVM屏障类型

LoadLoad:上面一个读操作,中间LoadLoad屏障,下面一个读操作,那么这两个操作不能换顺序

StoreStore:上面一个写操作,中间StoreStore屏障,下面一个写操作,那么这两个操作不能换顺序

LoadStore:上面一个读操作,中间LoadStore屏障,下面一个写操作,那么这两个操作不能换顺序

StoreLoad:上面一个写操作,中间StoreLoad屏障,下面一个读操作,那么这两个操作不能换顺序

5.4 Volatile修饰的变量

  Volatile保证了可见性和有序性

  可见性就是指当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改。volatile变量做到了这一点。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量还是volatile变量都是如此。普通变量与volatile变量的区别是,volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。因此我们可以说volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点

  在对Volatile修饰的变量的写操作前面会加一个StoreStore屏障,后面加一个StoreLoad屏障

  在对Volatile修饰的变量的读操作后面会加一个LoadLoad屏障和一个LoadStore屏障

  这样子保证了有序性

5.5 屏障的底层实现

  是通过lock指令实现的

  

线程基础知识09-JAVA的可见性和有序性问题的更多相关文章

  1. java线程基础知识----线程与锁

    我们上一章已经谈到java线程的基础知识,我们学习了Thread的基础知识,今天我们开始学习java线程和锁. 1. 首先我们应该了解一下Object类的一些性质以其方法,首先我们知道Object类的 ...

  2. java线程基础知识----线程基础知识

    不知道从什么时候开始,学习知识变成了一个短期记忆的过程,总是容易忘记自己当初学懂的知识(fuck!),不知道是自己没有经常使用还是当初理解的不够深入.今天准备再对java的线程进行一下系统的学习,希望 ...

  3. Java并发之线程管理(线程基础知识)

    因为书中涵盖的知识点比较全,所以就以书中的目录来学习和记录.当然,学习书中知识的时候自己的思考和实践是最重要的.说到线程,脑子里大概知道是个什么东西,但很多东西都还是懵懵懂懂,这是最可怕的.所以想着细 ...

  4. 《Java基础知识》Java线程的概念

    按照规划,从本篇开始我们开启『并发』系列内容的总结,从本篇的线程开始,到线程池,到几种并发集合源码的分析,我们一点点来,希望你也有耐心,因为并发这块知识是你职业生涯始终绕不过的坎,任何一个项目都或多或 ...

  5. java线程基础知识整理

    目录 线程基本概念 1.java实现线程 2.线程的生命周期 3.线程常用的方法 3.1.sleep() 3.2.interrupt方法 3.3.stop方法 4.线程调度 4.1.常见的线程调度模型 ...

  6. Java__线程---基础知识全面实战---坦克大战系列为例

    今天想将自己去年自己编写的坦克大战的代码与大家分享一下,主要面向学习过java但对java运用并不是很熟悉的同学,该编程代码基本上涉及了java基础知识的各个方面,大家可以通过练习该程序对自己的jav ...

  7. java 程序运行的基础知识【Java bytecode】

    聊聊文字,写一篇关于 java 基础知识的博文. JVM 线程栈 到 函数运行 每一个JVM线程来说启动的时候都会创建一个私有的线程栈.一个jvm线程栈用来存储栈帧,jvm线程栈和C语言中的栈很类似, ...

  8. java 基础知识三 java变量

    java  基础知识 三 变量 1.作用域 {} 包围起来的代码 称之为代码块,在块中声明的变量只能在块中使用 2.常量 就是固定不变的量,一旦被定义,它的值就不能再被改变. 3.变量 变量必须在程序 ...

  9. Windows核心编程 第六章 线程基础知识 (上)

    第6章 线程的基础知识 理解线程是非常关键的,因为每个进程至少需要一个线程.本章将更加详细地介绍线程的知识.尤其是要讲述进程与线程之间存在多大的差别,它们各自具有什么作用.还要介绍系统如何使用线程内核 ...

  10. Java线程基础知识(状态、共享与协作)

    1.基础概念 CPU核心数和线程数的关系 核心数:线程数=1:1 ;使用了超线程技术后---> 1:2 CPU时间片轮转机制 又称RR调度,会导致上下文切换 什么是进程和线程 进程:程序运行资源 ...

随机推荐

  1. ANSYS安装教程

    ANSYS 16.0 WIN10 64位安装步骤:1.使用"百度网盘客户端"下载ANSYS 16.0软件安装包到电脑磁盘里全英文名称文件夹内,安装前先断开网络,然后找到ANSYS. ...

  2. JUnit 5 单元测试教程

    点赞再看,动力无限. 微信搜「程序猿阿朗 」. 本文 Github.com/niumoo/JavaNotes 和 未读代码博客 已经收录,有很多知识点和系列文章. 在软件开发过程中,我们通常都需要测试 ...

  3. c++小练习——黑白棋

    没什么好发的,发给黑白棋水一水,如果有人能发现问题就更好了. /* Othello.cpp 黑白棋,实现随时结束并判断胜负的功能 成功运行于Visual Studio 2013 */ #include ...

  4. Day17.1:静态与非静态的详解

    静态与非静态 静态方法--类方法 是以static为关键词,从属于类,与类共生 public class Students{//class修饰的是一个类,所以这是一个学生类 public static ...

  5. day22-web开发会话技术04

    WEB开发会话技术04 14.Session生命周期 14.1生命周期说明 public void setMaxInactiveInterval(int interval):设置session的超时时 ...

  6. linux配置 python 开发环境sublime text及一些使用心得

    前言 一直以来我都使用 sublime text 作为主流开发的 ide ,但其实我开始在我的 linux mint 系统使用 sublime text 配置 python3 的开发环境踩过的坑又何止 ...

  7. Visual Studio高版本 ArcObject for .Net 低版本

    在基于ArcGIS的开发中,经常会存在Visual Studio版本高,ArcObject for .Net 版本低的问题.例如Visual Studio 2015的环境下,安装ArcObject f ...

  8. 记录一次 MyBatis 批量插入的优化-BatchInsert

    记录在一次项目问题排查过程中,遇到在数据量大的情况下,向数据库批量插入非常耗时长的问题. 1.分析 首先,代码是在 service 中,采用的是 for 循环调用 insert 语句的方式: for( ...

  9. 嵌入式Linux Qt移植详细过程

    嵌入式Linux下的Qt移植详细过程 开发说明 前段时间需要用开发板写一个下位机程序,是基于Linux系统,就想着用Qt来写,于是上网找教程看如何移植到开发板上.由于我不熟悉嵌入式Linux,加上网上 ...

  10. 关于Window中右键新建菜单的设置

    在日常工作中,有时候需要根据自己的需要对Windows桌面右键新建菜单进行自定义,自定义的方法如下: 1.在Window中输入Win+R调出运行命令框 2.输入regedit,打开注册表 3.如果想要 ...