发布和溢出
- 前言
- 前置知识
- 发布
- 溢出
- 简述
- 类型
- 案例
- 环境
- 发布
- 溢出
- 溢出类型1——未完全初始化就企图获取该对象中数据
- 溢出类型2——在构造函数中调用非private和final的方法
前言
回顾《Java并发编程实战》,这里附上一些总结和小案例加深理解。这里重点是对溢出的阐释。
前置知识
发布
发布一个对象,也就是使得其他代码块能够取得并使用该对象。最常见的发布方式就是将对象存储到公共静态域中。像Java Web中consts目录下的public static
类型的变量就可以看作是对这些对象的发布。
溢出
简述
由于不当的发布,就会引起溢出。如上述将对象的存储到公共静态域中,那么任何类和线程都能看见这个域,在这个过程中就可能发生逃逸——所有该发布的对象中所能获取的数据都变成公有了。
类型
- 对象中原本不该暴露的、私有的数据,因为该对象的发布,导致其他类或者线程能够通过该对象获取到其中的私有数据,那么这些私有数据就等于是间接的溢出了。
- 类的构造函数在构造对象时,尚未完全构造,就已经隐式地将自身(this)引用暴露出去了,导致了运行过程中出现了未知错误。这里有两种形式,具体看下面代码。
案例
环境
Maven3.X、JDK17
发布
这里展示最简单的一个发布案例,现在的URL就是所有的类和线程都能进行URL的使用了。
public class UrlConsts {
public static final String URL = "www.baidu.com";
}
溢出
溢出类型1——未完全初始化就企图获取该对象中数据
这里ThisEscape 构造过程中,隐含的就把this引用给了CustomizeListener
,如果线程执行过程中,val在doSomething
前尚未初始化,打印出来的val结果就是0。
public class EscapeTest {
@Test
public void test() {
new MethodEscapeTest ("qyl");
}
}
class ThisEscape {
private int val;
public ThisEscape() {
// 构造还未完成就已经发布了this引用,导致val还未初始化就被CustomizeListener调用了打印val的方法。
new CustomizeListener ( ) {
@Override
public void run() {
doSomething ( );
}
}.run ( );
val = 10;
}
// 溢出1,val大概率是0,属于未完成初始化就this引用溢出了
public void doSomething() {
System.out.println (val);
}
}
class CustomizeListener implements Runnable {
@Override
public void run() {}
}
执行结果如下:
解决方案:延迟线程的启动,通过工厂模式来解决。
public class EscapeTest {
@Test
public void test2(){
CustomizeListener listener = ThisEscape2.newInstance ( );
listener.run ();
}
}
class ThisEscape2{
private int val;
private final CustomizeListener listener;
private ThisEscape2() {
listener = new CustomizeListener (){
@Override
public void run() {
System.out.println (val);
}
};
this.val = 10;
}
public static CustomizeListener newInstance(){
ThisEscape2 escape2 = new ThisEscape2 ( );
return escape2.listener;
}
}
class CustomizeListener implements Runnable {
@Override
public void run() {
System.out.println ("hello");
}
}
运行结果如下:
溢出类型2——在构造函数中调用非private和final的方法
在初始化子类的时候,隐含着先对父类无参构造函数的调用,而父类的构造函数又调用了methodEscape
,该方法被子类重写了,所以会去执行子类重写后的方法,那么很明显name是未进行初始化的,所以导致name这里出现了问题,始终是null,通过private和final可以解决这个问题的出现。
public class EscapeTest {
@Test
public void test() {
new MethodEscapeTest ("qyl");
}
}
class ThisEscape {
public ThisEscape() {
System.out.println ("invoke father's constructor");
// 构造函数中调用非 final 和 private的方法,导致溢出
methodEscape ( );
// private 来解决上述问题
methodEscapeSolution1();
// final 解决上述问题
methodEscapeSolution2();
}
// 溢出2. 方法可能被重写,那么可能this引用溢出,导致后面的方法调用有问题
public void methodEscape() {
System.out.println ("call father's method");
}
// 正常功能 通过private解决构造函数中调用方法出现this引用溢出
private void methodEscapeSolution1(){
System.out.println ("call father's method1");
}
// 正常功能 通过 final 解决构造函数中调用方法出现this引用溢出
public final void methodEscapeSolution2(){
System.out.println ("call father's method2");
}
}
class MethodEscapeTest extends ThisEscape {
private final String name;
public MethodEscapeTest(String name) {
this.name = name;
System.out.println ("invoke son's constructor");
}
@Override
public void methodEscape() {
System.out.println ("son's name ----> " + name);
}
}
该代码块执行结果如下:
由于修正后的method1、method2子类无法重写所以不会产生问题。