原创:陶朱公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方法有可能会得到如下结果:
说明静态成员变量(jdk8位于堆内存中),因为被所有线程共享,所以本身也是线程不安全的!
本文完!
作者简介:陶朱公Boy (taozhugongboy),一二线互联网JAVA技术专家。欢迎关注我的微信公众号:『陶朱公Boy』,我们一起进步、一起成长!