一个多线程并发引发的关于JVM的思考

编程

问题提出

首先问题提出:如何让两个线程交替打印1-100的数字?

这里可能想到用wait和notify机制来快速编写代码,代码如下:

static class Solution implements Runnable {

int value = 0;

@Override

public void run() {

while (value < 100) {

synchronized (Solution.class) {

System.out.println(Thread.currentThread().getName() + ":" + value++);

Solution.class.notify();

try{

Solution.class.wait();

}catch (Exception e){

e.printStackTrace();

}

}

}

}

}

public static void main(String[] args) {

new Thread(new Solution(), "偶数").start();

new Thread(new Solution(), "奇数").start();

}

是否觉着代码看着没问题,当我们通过 Java 运行以上代码时,打印出来的数字却不对,如下所示:

偶数:0

奇数:0

偶数:1

奇数:1

偶数:2

奇数:96

......

偶数:97

奇数:97

偶数:98

奇数:98

偶数:99

奇数:99

实际分析 Java 代码是如何在 JVM 中运行的整个处理过程如下:

  1. JVM 向操作系统申请内存,JVM 第一步就是通过配置参数或者默认配置参数向操作系统申请内存空间,根据内存大小找到具体的内存分配表,然后把内存段的起始地址和终止地址分配给 JVM,接下来 JVM 就进行内部分配。

  2. JVM 获得内存空间后,会根据配置参数分配堆、栈以及方法区的内存大小。

  3. class 文件加载、验证、准备以及解析,其中准备阶段会为类的静态变量分配内存,初始化为系统的初始值。

  4. 完成上一个步骤后,将会进行最后一个初始化阶段。在这个阶段中,JVM 首先会执行构造器<client>方法,编译器会在.java文件被编译成.class 文件时,收集所有类的初始化代码,包括静态变量赋值语句、静态代码块、静态方法,收集在一起成为<client> () 方法。

  5. 执行方法。启动 main 线程,执行 main 方法,开始执行第一行代码。此时堆内存中会创建一个 Solution 对象,对象引用 solution 就存放在栈中,然后接着创建第二个Solution对象。

  6. 因为这是两个Solution对象,虽然都存储在堆内,但是它们的成员变量却不共享

  7. 最后启动线程,传入solution,运行run方法。

画图示意如下:

下面来验证我们的猜想,通过反编译看下具体字节码的实现,运行以下反编译命令,就可以输出我们想要的字节码:

//先运行编译class文件命令

> javac -encoding UTF-8 SolutionTest.java

//再通过javap打印出字节文件

> javap -v SolutionTest.class

通过输出的字节码,你会发现字节码如下所示:

看似一个简单的命令执行,但是前期编译的过程其实是非常复杂的,包括词法分析、填充符号表、注解处理、语法分析以及生成 class 文件,这个过程暂且不用过多去关注。主要从上图中便可知道,编译后的字节码文件主要包括常量池和方法表集合这两个部分就可以了。

  • 常量池主要记录的是类文件中出现的字面量以及符号引用。字面常量包括字符串常量(例如 String str="abc",其中 "abc" 就是常量),声明为 final 的属性以及一些基本类型(例如,范围在 -127~128 之间的整型)的属性。符号引用包括类和接口的全限定名、类引用、方法引用以及成员变量引用(例如 String str="abc",其中 str 就是成员变量引用)等。
  • 方法表集合中主要包含一些方法的字节码、方法访问权限(public、protect、prviate 等)、方法名索引(与常量池中的方法引用对应)、描述符索引、JVM 执行指令以及属性集合等。

注:当一个类被创建实例或者被其它对象引用时,虚拟机在没有加载过该类的情况下,会通过类加载器将字节码文件加载到内存中。在类加载后,class 类文件中的常量池信息以及其它数据会被保存到 JVM 内存的方法区中。类在加载进来之后,会进行连接、初始化,最后才会被使用。

可以很明显看到,run() 方法中 getfield #3 Field value 1 以及 putfield #3 Field value 1,然后参照对比之后的反编译的字节码图。

解决问题

解决办法:可以将两个线程改为共用同一个对象即可解决。代码如下:

Solution solution = new Solution();

new Thread(solution, "偶数").start();

new Thread(solution, "奇数").start();

改完发现结果是对上了,但你仔细想想还有没有更好的方法或者其他方法呢?这里直接给出答案了

不用改动new的两个对象,可以将类的成员变量改成静态变量,代码如下:static int value = 0;将其运行,也能得到正确的答案。

至于为什么?可以详细分析一波

  1. 首先,运行java代码的JVM虚拟机内部存储结构分为了5个部分,分别为堆、系统栈、方法栈、方法区、程序计数器
  2. 其次,静态变量保存在方法区内,而方法区内的对象是可以共享的,如下图所示:

  1. 因此value对象的值可以供两个对象使用,而不会发生变化

如下图所示:

再次通过反编译手段,来验证我们的猜想。

  • 可以很明显看到,run()方法中getstatic #3 Field value 1 以及putstatic #3 Field value 1

总结

了解完实际代码在 JVM 中分配的内存空间以及运行原理,相信你会更加清楚内存模型中各个区域的职责分工。

以上是 一个多线程并发引发的关于JVM的思考 的全部内容, 来源链接: utcz.com/z/515890.html

回到顶部