Java内部类、局部类的实现原理以及与内存可见性的关系

实现原理

以下内容一部分来自于core java第十版,一部分来自于我使用openjdk java1.8/java11的javac和fernflower这个反编译器反编译字节码得到的
以下内容不确实是openjdk javac特有的实现,还是规范这样要求

  • 对象内总有一个隐式引用, 它指向了创建它的外部类对象,比如下面的反编译代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class JavaLangTest {
    public JavaLangTest() {
    super();
    }

    public static void main(String[] var0) {
    JavaLangTest.InnerClass var1 = new JavaLangTest.InnerClass(new JavaLangTest());
    System.out.println(var1);
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    class JavaLangTest$InnerClass {
    // $FF: synthetic field
    final JavaLangTest this$0;

    private JavaLangTest$InnerClass(JavaLangTest var1) {
    super();
    this.this$0 = var1;
    }
    }

    其对应与以下代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class JavaLangTest {

    public static void main(String[] args) {
    InnerClass innerClass = new JavaLangTest().new InnerClass();
    System.out.println(innerClass);
    }

    private class InnerClass {
    }
    }
  • 内部类是一种编译器现象,与虚拟机无关。编译器将会把内部类翻译成用$(美元符号)分隔外部类名与内部类名的常规类文件,而虚拟机则对此一无所知

  • 内部类可以访问外围类的私有数据——无论是否static。这是一个编译器现象,那么也就是实际上这个内部类并没有魔法加持,那么它是如何访问外部类的private数据的?外部类会合成一个类似于

    1
    2
    3
    static int access$100(JavaLangTest x0) {
    return x0.a;
    }

    的方法,然后内部类调用这个方法,传递this$0这种指向外部类的引用的参数,从而获得private的数据(如果是static内部类,那么并不需要传递参数就可以获取)。可以利用这个特性,在无关的地方,使用反射来调用这个方法从而获取该类的private数据

  • 局部类的实现原理

    • 这是反编译字节码得到的局部类
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      class JavaLangTest$1LocalClass {
      // $FF: synthetic field
      final InnerClass val$innerClass;
      // $FF: synthetic field
      final int val$a;

      JavaLangTest$1LocalClass(InnerClass var1, int var2) {
      super();
      this.val$innerClass = var1;
      this.val$a = var2;
      }

      void worker() {
      System.out.println(this.val$innerClass);
      System.out.println(this.val$a);
      }
      }
    • 这是原来的代码
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      InnerClass innerClass = new JavaLangTest().new InnerClass();
      innerClass.worker();
      int a = 10;
      class LocalClass {
      void worker() {
      System.out.println(innerClass);
      System.out.println(a);
      }
      }
      LocalClass localClass = new LocalClass();
      localClass.worker();

内存可见性

内部类
  • 既然我们已经知道内部类访问外部类的原理,那么内存可见性其实就和普通的类之间互相访问对方的数据没有差别。所以,如果一个变量没有final修饰或者没有volatile修饰或者没有加锁,那么就不能保证其对内部类是可见的,即使可见,也不能保证内部来看到的对象是完成构造之后的对象(这里有个问题,原子变量也是吗)
  • 按照JCIP(java concurrent in practice),一个对象的引用即使对于某个线程是可见的(比如某个对象发现这个引用非null了),这个对象的状态有可能还没初始化完成,也就是这个对象可能处于不一致状态
  • 不过我在测试中,因为不能对该对象设置volatile,也不能搞个volatile的flag来标志这个对象是否已经完成初始化(因为JSR133保证,某个volatile变量被Thread a 写入后,Thread b去读这个变量,读了之后,原先在Thread a写入该变量之前对于Thread a可见的状态,对于Thread b都可见),所以情况一直是读到该变量是null,即使另一个线程已经初始完成。所以没有复现出JCIP中提到的这种情况
局部类
  • 因为创建线程过程中,是先构造一个Runnable对象,然后在传递给Thread,也就是构造对象的过程是在原来线程中进行的,所以可以读到这个事实final变量的正确的值
  • 这是手写的代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    class Test3 {
    public static void main(String[] args) {
    int a = 10;
    new Thread(new Runnable() {
    @Override
    public void run() {
    System.out.println(a);
    }
    });
    new Thread(() -> {
    System.out.println(a);
    });
    }
    }
  • 这是反编译得到的代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    import misc.Test3.1;

    class Test3 {
    Test3() {
    super();
    }

    public static void main(String[] args) {
    int a = 10;
    new Thread(new 1(a));
    new Thread(new Runnable() {
    public run() {
    System.out.println(a);
    }
    });
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    final class Test3$1 implements Runnable {
    // $FF: synthetic field
    final int val$a;

    Test3$1(int var1) {
    super();
    this.val$a = var1;
    }

    public void run() {
    System.out.println(this.val$a);
    }
    }
  • 虽然lambda反汇编出来跟匿名内部类的代码不太一样,不过我认为也是同样的在同一个线程构造Runnable对象后再传递进去(欢迎指正!)