欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

Java内存模型与线程

程序员文章站 2022-05-04 21:42:27
...

参考

《深入理解java虚拟机-jvm高级特性与最佳实践》第12章Java内存模型与线程
《java并发编程的艺术》第3章 java内存模型

服务性能好坏的高低指标:

TPS Transactions Per Seconds每秒处理事务数
代表一秒内服务端平均能相应的请求总数

物理计算机的并发问题
复杂性来源是 绝大部分的运算任务都不可能只靠处理器计算就能完成,处理器至少要与内存交互,如读取运算数据,存储运算结果等,这个io是很难消除的。
基于内存交互的复杂性,所以引入新的基于 高速缓存的存储交互 解决了处理器与内存的速度矛盾,但是带来新的问题: 缓存一致性
为了解决缓存一致性,引入内存模型这一概念


内存模型
    可以理解为在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象

Java的内存模型
java虚拟机定义的规范,用来屏蔽各种硬件和操作系统的内存访问差异,以实现让java在各个平台下都能达到一致的内存访问效果。
如图:
Java内存模型与线程

分几个部分: java线程,工作内存,save和load8种操作和主内存 以及变量
java的线程有自己的工作内存,工作内存中保存了该线程用到的变量的主内存副本拷贝,
所有的变量都存储在主内存中,线程只能对自己的工作线程中的变量副本进行读取,赋值等操作,无法直接操作主内存
主内存中存储的变量跟java代码中的变量不一样
此处变量包括 实例字段,静态字段和构成数组对象的元素不包括局部变量和方法参数,
工作内存与主内存之间通过save和load8种操作来交互,这8种操作都是原子性的

工作内存与主内存的交互操作以及相关规则:

  • lock(锁定):作用于主内存的变量,把一个变量标识为一条线程独占状态。
  • unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作。
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
  • store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
  • write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。

    使用的规则:

  • 不允许read和load、store和write操作之一单独出现
  • 不允许一个线程丢弃它的最近assign的操作,即变量在工作内存中改变了之后必须同步到主内存中。
  • 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中。
  • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量。即就是对一个变量实施use和store操作之前,必须先执行过了assign和load操作。
  • 一个变量在同一时刻只允许一条线程对其进行lock操作,lock和unlock必须成对出现
    如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
  • 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
  • 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)。

先行发生happens-before

先行发生是Java内存模型中定义的两项操作之间的偏序关系, 是用来判断数据是否存在竞争,线程是否安全的主要依据。
之前我们判断线程安全是用竞态条件来判断,主要就是先检查后执行,
竞态条件: 基于一种可能失效的观察结果来做判断或者执行某个计算。

先行发生如何来判断线程安全?

如果多个线程对同一个变量的操作并没有明显的先行发生关系,那么这个变量就有可能是线程不安全的。

java内存模型中默认的先行发生关系

  • 程序次序关系 在一个线程中,按照程序代码顺序,书写在前的操作先行发生于书写在后面的操作,如果考虑分支,循环等结构,应该是控制流顺序而不是程序代码顺序。
    指令重排序难道不会影响程序次序关系吗?
    在同一个线程中,有如下代码
   int i = 0;
   int j = 0;
i赋值的语句不一定发生在给j赋值的语句之前,这是由指令的重排序决定的,对于
程序次序关系 这一规则来说,由于在这个线程中无法感知到这一点,所以并不影响它的正确性
  • 管程锁定规则 一个unlock操作先行发生于后面对同一个锁的lock操作。
  • volatile变量规则 对一个volatile变量的写操作先行发生于后面对于这个变量的读操作
    volatile修饰的变量不一定是线程安全的,所以在多线程环境中对于当前变量的写操作不一定先行于其他线程对这个变量的读操作。所以这个规则适用于当前线程。
    如:
    public class VolatileDemo {
    private volatile int race = 0;

    public int getRace() {
        return race;
    }

    public void setRace() {
        this.race++;
    }

    public void test1(){
        try {
            for (int i = 0; i < 100; i++) {
                /**如果是2的倍数,对volatile进行写操作*/
                if(i % 2 == 0){
                    new Thread(() -> setRace()).start();
                }else{
                    /**读操作*/
                    new Thread(() -> getRace()).start();
                }
            }
            System.out.println(getRace());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void test2(){
        getRace();
        for (int i = 0; i < 100; i++) {
            setRace();
        }
        System.out.println(getRace());
    }

    public static void main(String[] args) {
        VolatileDemo volatileDemo = new VolatileDemo();
        try {
            volatileDemo.test2();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
test1的结果 : 16
test2的结果: 100
  • 线程启动规则 thread的start方法先行发生于此线程的每一个动作
  • 线程终止规则 线程中的所有操作都先行发生于对此线程的终止检测。
  • 线程中断规则 对线程interrupt方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 对象终结规则 一个对象的初始化完成先行发生于他的finalize方法的开始
  • 传递性 如果a先行于b,b先行于c,那么a肯定先行于c

内存分配过程

首先要理解一个概念: 逃逸分析

逃逸分析,是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法

通过逃逸分析 可以得到当前对象是分配到堆上还是分配到栈上

逃逸分析并不是直接的优化手段,而是一个代码分析,通过动态分析对象的作用域,为其它优化手段如栈上分配、标量替换和同步消除等提供依据,发生逃逸行为的情况有两种:方法逃逸和线程逃逸。
1、方法逃逸:当一个对象在方法中定义之后,作为参数传递到其它方法中;
2、线程逃逸:如类变量或实例变量,可能被其它线程访问到;

如果不存在逃逸行为,则可以对该对象进行如下优化:同步消除、标量替换和栈上分配。

如何开启?
开启
-server -XX:+DoEscapeAnalysis -XX:+PrintGCDetail -Xmx10m -Xms10m
关闭
-server -XX:-DoEscapeAnalysis -XX:+PrintGCDetail -Xmx10m -Xms10m

内存分配过程

  1. 通过逃逸分析,确定对象是在堆上还是在栈上,如果是在栈上则跳到步骤4
  2. 如果是在堆上,通过tlab_top + size <= tlab_end 判断当前线程缓冲区是否有足够的空间存放当前对象,如果不足,则跳到步骤3
  3. 重新申请一个TLAB, 再次尝试存放当前对象,如果还是放不下,则跳到步骤4
  4. 在eden区加锁(eden区位于堆中,是一个所有线程共享的区域,所以要进行加锁操作),根据eden_top + size <= eden_end 判断eden区是否有足够空间存放当前对象,如果有,增加eden_top的值,如果不足,则跳到步骤5
  5. 执行一次young_gc
  6. 经过young_gc之后,如果仍然不能存放当前对象,则直接分配到老年代
相关标签: concurrent jvm