点击上方蓝色 “猿芯” 关注,输入1024,你懂的
最近在阅读 ThreadLocal
源码的时候,发现一段很有意思的代码,代码片段如下:
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
以上代码片段主要是 ThradLocal
生成哈希值(threadLocalHashCode
)的逻辑,通过静态的原子整型变量 nextHashCode
以及静态方法 nextHashCode ()
,为每个线程持有的 ThreadLocal
本地变量生成唯一 的 hashCode
。
注:ThreadLocal
的 hashCode
选择 HASH_INCREMENT
变量值:0x61c88647
很有意思,里面涉及到斐波那契数列
和黄金分割法
,感兴趣的同学可以自行了解下。
当然本文的重点不是 ThreadLocal
原理分析上,而是分析 static
关键字修饰的静态域(静态变量、静态块)顺序加载问题。
这段代码总共四行,除了第一行都是用 static
关键字修饰的,这里我们设想一个问题,当类初始化的时候,这四行代码是从上往下执行的吗?
答案是:”否“。
为了方便 debug
调试,我们把上面的代码稍微做了下调整,代码片段如下:
public class Static01 {
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = getIncr();
public Static01(){
System.out.println("threadLocalHashCode::" + threadLocalHashCode);
}
private static int getIncr() {
return 0x61c88647;
}
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
public static void main(String[] args) {
new Static01();
}
}
上面的代码片段用 debug
模式启动,通过为每行代码打断点,发现当真正实例化 Static01
类时,代码运行顺序并非是按照逐行执行,而是如下图红色标记顺序进行的。
其执行流程是:
new
关键字初始化 Static01
类的构造方法nextHashCode
HASH_INCREMENT
threadLocalHashCode
Static01
构造方法打印 threadLocalHashCode
变量的 hash
值对象实例化
就是执行类中构造函数的内容,如果该类存在父类,会通过显示或者隐示的方式(
super
方法)先执行父类的构造函数,在堆内存中为父类的实例变量开辟空间,并赋予默认的初始值,然后在根据构造函数的代码内容将真正的值赋予实例变量本身,然后,引用变量获取对象的首地址,通过操作对象来调用实例变量和方法
从上面代码执行流程可以看出
static
修饰的静态变量,并且静态变量也是有加载顺序的;public Static01() {
super();
System.out.println("threadLocalHashCode::" + threadLocalHashCode);
}
另外,静态语句块中只能访问到定义在静态块之前的变量,在静态块里可以给该变量赋值,但是不能访问,否则编译器会提示 “Illegal forward reference
” 错误,如下图
静态块主要用于类的初始化,不是指对象的实例化。它只会执行一次,静态块只能访问类的静态成员属性和方法,不能在静态块使用 this
。
我们先把上面的代码稍加改造下,增加 “静态块1
”和“静态块2
” 静态块代码
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
static{
System.out.println("静态块1");
}
private static final int HASH_INCREMENT = getIncr();
static{
System.out.println("静态块2");
}
运行结果如下:
发现不管是静态块还是静态变量,它们之间都是按顺序执行的。那为什么是静态块、静态变量的初始化是有顺序的呢?
通过查看 Static01
类的 class
编译文件,发现编译器会把 static
块的代码放在同一 static
花括号{}
内。
代码顺序是按照之前编码的顺序整合,这么看来是编译器在作怪吧。
static {
System.out.println("静态块1");
HASH_INCREMENT = getIncr();
System.out.println("静态块2");
}
从《深入理解Java虚拟机:JVM高级特性与最佳实践(第2版)》这本书讲的类加载机制原理可知:
当遇到
new
、getstatic
和putstatic
或invokestatic
这4
条字节码指令时,如果类没有进行过初始化,则需要先触发其初始化。
这就解释了为什么在对象未实例化前,可以通过 “类名.静态属性变量、类名.静态方法” 的方式访问静态变量和静态方法了。
类加载的时机
对于初始化阶段,虚拟机规范规定了有且只有 5 种情况必须立即对类进行“初始化”(而加载、验证、准备自然需要在此之前开始):
1、静态域(静态变量、静态块)是按逐行顺序加载的,并且静态域只会加载一次。
2、当实例化对象之前(构造方法调用),会先去初始化静态域,再去调用构造函数实例化对象。
3、一般对象初始化顺序如下:父类的静态域顺序加载–>子类静态域顺序加载–>父类非静态域初始化->父类构造函数初始化–>子类非静态域初始化->子类构造函数初始化。
最近整理一份很全的
Java
学习资料,感兴趣的老铁可以在微信搜索【猿芯】,后台回复以下关键字,即可免费获取。回复“sb
”,免费获取SpringBoot
全套视频教程。回复“sc
”,免费获取SpringCloud
全套视频教程。回复“面试
”,免费获取Java
面试全套题集。回复“小程序
”,免费获取微信小程序开发
全套视频教程。
作者简介:
编筐少年
,一枚简单的北漂程序员。喜欢用简单的文字记录工作与生活中的点点滴滴,愿与你一起分享程序员灵魂深处真正的内心独白。我的微信号,输入1024
,有份惊喜送给你哦。
如果您觉得本文对你有帮助,欢迎老铁们帮忙:点赞、在看、留言、分享,你们的支持是我原创最大的动力。
【猿芯】