JVM内存模型

2022/02/04 posted in  JVM

起因

前段时间我们把一个站点做了一些精简及内存优化。 精简主要集中在无用接口做下线处理,重复代码做抽象等等。内存优化主要集中在对之前不合理的内存设置做调整。

之前站点的内存设置特别大,但实际站点在精简后用很大的内存有点浪费,所以做了一下内存的缩小。 在做内存调整的时候,需要去确认java进程所需的内存大小,也就是最终需要设置的xmx/xmn等值。在做测试的时候,发现了一个内存可见性的问题。 于是便有了下面这部分的内容整理。

PS:

以下内容来自组内同学 俺是小灰灰 总结整理。

java内存模型的背景

由于计算机的存储设备与处理器的运算速度有几个数量级差距,所以现代计算机系统都不得不加入一层读写速度尽可能接近处理器运算速度的高速缓存来作为内存与处理器之间的缓冲,将运算需要使用到的数据复制到缓存中,让运算能力快速进行,当运算结束后再从缓存同步回内存中,这样子处理器就无须等待缓慢的内存读写了。

由此引入了一个新的问题:缓存一致性

img

不同平台上内存模型的差异,有可能导致程序在一套平台上开发完全正常,而在另一套平台上并发访问却经常出问题,所以jvm虚拟机试图定义一种java内存模型来屏蔽各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台上都能达到一致的内存访问效果。

JMM(Java Memory Model

img

变量

此处的变量与java编程中的变量有所区别,它包括了实例字段、静态字段和构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的。

Happens-before Order

主内存与工作内存之间具体的交互协议,有一个等效的判断原则就是先行发生原则,用来确定第一操作对第二个操作的可见性。

1、程序次序规则。在一个线程内,书写在前面的代码先行发生于后面的。确切地说应该是,按照程序的控制流顺序,因为存在一些分支结构。

2、Volatile变量规则。对一个volatile修饰的变量,对他的写操作先行发生于读操作。

3、线程启动规则。Thread对象的start()方法先行发生于此线程的每一个动作。

4、线程终止规则。线程的所有操作都先行发生于对此线程的终止检测。

5、线程中断规则。对线程interrupt()方法的调用先行发生于被中断线程的代码所检测到的中断事件。

6、对象终止规则。一个对象的初始化完成(构造函数之行结束)先行发生于发的finilize()方法的开始。

7、传递性。A先行发生B,B先行发生C,那么,A先行发生C。

8、管程锁定规则。一个unlock操作先行发生于后面对同一个锁的lock操作。

举个例子说明下

private int value = 0;
public void setValue(int value){ 
  this.value = value;
}
public int getValue(){ 
  return value;  
}

上述代码显示的是再普通不过的get/set方法,假设存在线程A和B,线程A(时间上的先后)调用了setValue(1),然后线程B调用了getValue()方法,那么线程B返回的是什么?

因为是两个线程所以程序次序规则明显不符合,没有使用volatile关键字第二个规则也不符合,依次类推剩余的规则也不符合,所以不能确认线程B调用方法的返回结果。

此外在这里说下跟可见性相关的三个关键字

synchronized

在Doug Lea大神的Concurrent Programming in Java

http://gee.cs.oswego.edu/dl/cpj/index.html

In essence, releasing a lock forces a flush of all writes from working memory employed by the thread, and acquiring a lock forces a (re)load of the values of accessible fields. While lock actions provide exclusion only for the operations performed within a synchronized method or block, these memory effects are defined to cover all fields used by the thread performing the action.

简单来说:当线程释放一个锁时会强制性的将工作内存中之前所有的写操作都刷新到主内存中去,而获取一个锁则会强制性的加载可访问到的值到线程工作内存中来。虽然锁操作只对同步方法和同步代码块这一块起到作用,但是影响的却是线程执行操作所使用的所有字段。

举个例子

public class NoVisibilityDemonstration extends Thread {    
  boolean keepRunning = true;
  public static void main(String[] args) throws Exception { 
      NoVisibilityDemonstration t = new NoVisibilityDemonstration();        
      t.start();        
      System.out.println("start" + t.keepRunning);        
      Thread.sleep(1000);        
      t.keepRunning = false;        
      System.out.println("end" + t.keepRunning);
    }
    public void run() {        
      int x = 1;
      while (keepRunning) {
        //            System.out.println("如果你不注释这一行,程序会正常停止!");            
        x++;        
      }
      System.out.println("x:" + x);    
    }
}

看下System.out.println方法的源码,因为synchronized关键字的同步语义起了作用

    /**     
    * Prints a String and then terminate the line.  This method behaves as     
    * though it invokes {@link #print(String)} and then     
    * {@link #println()}.     
    *     
    * @param x  The {@code String} to be printed.     
    */    
public void println(String x) {        
  synchronized (this) {            
    print(x);            
    newLine();        
  }    
}

ps:正常业务场景中肯定不要这么写代码......,多线程之间使用共享变量肯定是保证线程安全哈

留坑疑问

我多次尝试了下用Thread.sleep()方法取代System.out.println()也可以调出循环拿到最新的值。但是Thread.sleep 和 Thread.yield 都不具有同步的语义。在 Thread.sleep 和 Thread.yield 方法调用之前,不要求虚拟机将寄存器中的缓存刷出到共享内存中,同时也不要求虚拟机在这两个方法调用之后,重新从共享内存中读取数据到缓存。

volatile

有两层语义:第一是保证此变量对所有线程的可见性,是指当一条线程修改了这个变量的值,新值对于其他线程是立即可见的。使用场景在单列模式的双重加锁,这里再多说一句volatile 属性的读写操作都是无锁的,它不能替代 synchronized,因为它没有提供原子性和互斥性。第二层语义是禁止指令重排序,这里我们只讨论下内存可见性。

final

在 JSR-133 中的增强,由于 final 的值本身是不可被重写入的(所谓的“ 不变” 对象),那么编译器就可以针对这一点进行优化:

Compilers are allowed to keep the value of a final field cachedin a register and not reload it from memory in situations wherea non-final field would have to be reloaded.

但是,如果对象属性不使用 final 修饰,在构造器调用完毕之后,其他线程未必能看到在构造器中给对象实例属性赋的真实值(除非有其他可行的方式保证 happens-before 一致性)

A thread that can only see a reference to an object after thatobject has been completely initialized is guaranteed to see thecorrectly initialized values for that object’s final fields.

class FinalFieldExample {    
  final int x;    
  int y;    
  static FinalFieldExample f;     
  public FinalFieldExample() {        
    x = 3;        
    y = 4;    
  }     
  static void writer() {        
    f = new FinalFieldExample();    
  }     
  static void reader() {       
    if (f != null) {            
      int i = f.x; // guaranteed to see 3            
      int j = f.y; // could see 0       
    }    
  }
}

这个例子正式规范里面给出的,上面属性 x 使用了 final 修饰,而 y 没有,在某一时刻,一个线程调用 writer() 的时候,FinalFieldExample 被初始化;之后另一个线程去取得这个对象,首先最开始的时候 f 未必一定不为空,因为 f 并没有任何 happens-before 一致性保证(f 可能被赋了一个构造并未完成的对象),其次对于属性 x,由于 final 关键字的效应,f 不为空的时候,f 已经初始化完成,所以 f.x 一定为准确的 3,但是 f.y 就不一定了。

参考

https://javadoop.com/post/Threads-And-Locks-md#toc_18

https://www.raychase.net/1887

https://juejin.im/post/5c28d01ef265da61616ec8a5#heading-3

关于我及张二蛋又要扯蛋了

    一个不务正业的程序猿及这个程序猿写字的地方,这里可能有技术,有理财,有历史,有总结,有生活,偶尔也扯扯蛋,妥妥的杂货铺,喜欢可关注。
    酒已备好,等你来开
图片