面试官问:静态变量vs实例变量vs局部变量在JVM内存区域是怎么分配的?并发场景线程安全吗?

246 阅读5分钟

原创:陶朱公Boy(微信公众号ID:taozhugongboy),欢迎分享,转载请保留出处。

大家好,我是陶朱公Boy。

前言

今天跟大家分享一个面试题:

1)什么是类变量、什么是成员变量、什么是局部变量?

2)上述各个变量在内存区域中如何布局?线程安全问题如何?

面试官心理

首先 java中的类变量vs成员变量vs局部变量概念及区别你是否清晰明了。

其次题目中涉及的变量(静态变量、成员变量、局部变量)都分布在具体哪个内存区域?

最后多线程高并发场景下哪些变量能保证线程安全吗?哪些不能?

我们循序渐进的分析

名词解析

类变量vs成员变量vs局部变量

类变量: 也称静态变量,也就是在实例变量前加了static 的变量。类变量定义在类中但独立于方法和语句块之外,静态变量可以通过ClassName.VariableName的方式访问。类变量被声明为public static final类型时,即常量,类变量名称一般使用大写字母。

成员变量:成员变量又称实例变量。它被定义在类中但在任何方法之外,没有static修饰。类的每个对象维护它自己的一份成员变量的副本
1、成员变量定义在类中,在整个类中都可以被访问。
2、成员变量随着对象的建立而建立,随着对象的消失而消失,存在于对象所在的堆内存中。
3、成员变量有默认初始化值。

局部变量:局部变量声明在方法、构造方法或语句块中。它在方法、构造方法、或语句块被执行的时候创建,执行完成后被销毁。它的作用域也局限于方法、构造方法或者语句块中。访问修饰符不能用于局部变量
1、局部变量只定义在局部范围内,如:函数内,语句内等,只在所属的区域有效。
2、局部变量存在于栈内存中,作用的范围结束,变量空间会自动释放。
3、局部变量没有默认初始化值

内存布局

Java 8 的内存结构

实战分析

接下来的这段示例代码,我们类Demo01的main方法内部定义了一个类型A的局部变量a,在类A中定义了一个类变量width;一起看看它们在VM内存区域的分布:

首先:我们本地单测跑的是main方法,主线程调用main方法的时候我们知道会在VM虚拟机栈空间内创建一个栈帧数据结构。

栈帧(Stack Frame)是用来支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。

栈帧(Stack Frame)存储了方法的局部变量表、操作数栈、动态连接、和方法返回地址、额外的附加信息。

每个方法在执行的同时,都会创建一个栈帧(Stack Frame)。每一个方法从调用开始至执行完成的过程,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。

其次:这里有一个局部变量的引用a指向了A实例对象。这个A对象是被分配在堆内存空间的。

运行时新建的对象都会被分配在堆空间内

还有Class对象也是被分配在堆空间的。

最后:好像还剩一个静态成员变量,看看它会被分配在哪个内存区域呢? 答案是方法区。(java7及之后存在于堆中,之前存在于方法区内)

方法区:它主要存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等。

线程安全问题

线程安全问题:指多个线程对同一个对象中的同一个实例变量进行操作时,会出现值被更改、值不同步的情况,进而影响程序的执行流程

1)成员变量的线程安全问题?

public class ClassInstanceVariableThreadSafe {

private int number = 0;


public void increate() {
    number++;
}
public static void main(String[] args) {
    ClassInstanceVariableThreadSafe t = new ClassInstanceVariableThreadSafe();
    Thread[] threads=new Thread[20];
    //这里启动20个线程
    for (int i = 0; i < 20; i++) {
        threads[i]= new Thread(()->{
            //每个线程对同一个对象t内部的成员变量number变量进行10000次累加操作
            for(int j=0;j<10000;j++){

                t.increate();
            }
        });
        threads[i].start();
    }
    //等待所有累加线程都结束
    while (Thread.activeCount() > 1) {
        Thread.yield();
    }
    System.out.println(t.number);
}

}

多次执行Main方法有可能会得到如下结果:

上述这段代码发起20个线程,每个线程对同一个实例t发起10000次累加,按照我们的理解,最终的结果应该是200000。不过另你失望了,结果每次都不一样,甚至是我截图的数字。为什么?

只能说明一定,存在于堆内存中的成员变量,因为被所有线程共享,所以是线程不安全的。

接下来,我们再一起看一下静态变量的线程安全问题?

2)静态变量是否线程安全呢?

public class StaticVariableThreadSafeDemo {

private static int number;

public void increate() {
    number++;
}

public static void main(String[] args) {
  //  StaticVariableThreadSafeDemo staticVariableThreadSafeDemo=new StaticVariableThreadSafeDemo();
    Thread[] threads=new Thread[20];

    //这里启动20个线程
    for (int i = 0; i < 20; i++) {
        threads[i]= new Thread(()->{
            for(int j=0;j<10000;j++){
                //为了说明问题,每个线程我们新建10000个对象分别调用其increate方法
                new StaticVariableThreadSafeDemo().increate();
            }
        });
        threads[i].start();
    }
    //等待所有累加线程都结束
    while (Thread.activeCount()>1){
        //我的任务处理的差不多了,可以让给相同优先级的线程CPU资源了;不过确实只是一个暗示,没有任何机制保证它的建议将被采纳;
        Thread.yield();
    }
    System.out.println(number);
}

多次执行Main方法有可能会得到如下结果:

image.png

说明静态成员变量(jdk8位于堆内存中),因为被所有线程共享,所以本身也是线程不安全的!

本文完!

作者简介:陶朱公Boy  (taozhugongboy),一二线互联网JAVA技术专家。欢迎关注我的微信公众号:『陶朱公Boy』,我们一起进步、一起成长!