几个匿名内部类问题的思考

简介: 本博客主要探究匿名内部类的原理,重点讨论java是如何将对象传递进入匿名内部类的内部的?


本博客是由工作中遇到的一个bug而引起的对匿名内部的思考,分享这个case希望能够帮助大家理解匿名内部类的原理。

接下来我们通过实例看一下这个问题,以及实现原理。

 

 代码

1  Tester

此类主要逻辑:在此类的main方法中,先创建多个线程,每个线程中创建一个任务,然后将任务加入到线程池中执行;在线程池中的任务非常简单,即将任务所属的线程id输出。


public class Tester {
    private static ExecutorService executorService = Executors.newFixedThreadPool(3);
 
    public static void main(String[] args) {
 
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    TaskInfo task = new TaskInfo();
                    long threadId = Thread.currentThread().getId();
                    task.setThreadId(threadId);
 
                    executorService.execute(new Runnable() {
                        @Override
                        public void run() {
                            try {
                                // 这里休眠1s,保证外层的Thread先执行结束,再执行此Runnable内部的后续逻辑。
                                Thread.sleep(1000L);
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                            // task的的实例是如何注入进来的?
                            // task是线程安全的么?
                            long tid = task.getThreadId() ;
                            System.err.println("in ExecutorService thread id=" + tid);
                        }
                    });
                    System.err.println("in thread end id=" + task.getThreadId());
                    // 是否可以循环利用TaskInfo对象?
                }
            });
            thread.start();
        }
    }
}


 

2  TaskInfo

一个简单的pojo类,存储线程id。


public class TaskInfo {
    private long threadId;
 
    public long getThreadId() {
        return threadId;
    }
 
    public void setThreadId(long threadId) {
        this.threadId = threadId;
    }
}
 


 问题

在执行ExecutorService#execute(Runnable command)时,没有将TaskInfo的实例作为参数传入到Runnable中,执行Runnable代码的时候,为什么能够正常访问TaskInfo的实例,即能够执行【task.getThreadId()】这句代码?

在ExecutorService#execute(Runnable command)的Runnable中我将线程休眠了1秒,外层的Thread会先执行完,那么在执行【long tid = task.getThreadId() 】时,是否能够正确的输出threadId?如果能,为什么可以做到?这个实例(task)什么时候回收?

如果TaskInfo被循环利用是否会有线程安全问题?

Tip:如果对这几个问题感兴趣,建议先思考一下,然后再继续往后看。

 

 问题及原理分析

因为字节码内容太多,所以这里只截取与这里讨论问题相关的部分。请重点留意标红的内容。

1  TaskInfo的实例是怎么注入到ExecutorService#execute的Runnable实例中去的?

其实这是以上两个问题个根源,接下来我们对这个问题剖跟问底一下。

1)   Class文件

查看class文件时,发现有以下几个文件:TaskInfo.class、Tester.class、Tester$1.class、Tester$1$1.class。那么问题来了:Tester$1.class、Tester$1$1.class是哪来的?

让我们看看他们的字节码信息。

2)   Tester.class字节码

从main方法中的红色部分我们可以看到,在执行【new Thread(new Runnable)】时创建了一个Tester$1对象。另外,Tester.class字节码中,却没有找到【new Thread(new Runnable)】中Runnalbe#run方法的任何代码,这个也很是奇怪呀。

考虑到编译器不会将我们的代码无故丢弃,那么Tester$1中是不是就是Thread(new Runnable)】中Runnalbe#run的代码?


public class Tester

  minor version: 0

  major version: 52

  flags: ACC_PUBLIC, ACC_SUPER

Constant pool:

  #30 = Utf8               Tester$1

  #31 = Methodref          #29.#22        // Tester$1."<init>":()V

  #32 = Methodref          #27.#33        // java/lang/Thread."<init>":(Ljava/lang/Runnable;)V

  #33 = NameAndType        #20:#34        // "<init>":(Ljava/lang/Runnable;)V

  #34 = Utf8               (Ljava/lang/Runnable;)V

  #35 = Methodref          #27.#36        // java/lang/Thread.start:()V

  #36 = NameAndType        #37:#8         // start:()V

 

  public static void main(java.lang.String[]);

    descriptor: ([Ljava/lang/String;)V

    flags: ACC_PUBLIC, ACC_STATIC

    Code:

      stack=4, locals=3, args_size=1

         0: iconst_0

         1: istore_1

         2: goto          27

         5: new           #27                 // class java/lang/Thread

         8: dup

         9: new           #29                 // class Tester$1

        12: dup

        13: invokespecial #31                 // Method Tester$1."<init>":()V

        16: invokespecial #32                 // Method java/lang/Thread."<init>":(Ljava/lang/Runnable;)V

        19: astore_2

        20: aload_2

        21: invokevirtual #35                 // Method java/lang/Thread.start:()V

        24: iinc          1, 1

        27: iload_1

        28: bipush        10

        30: if_icmplt     5

        33: return

 

3)   Tester$1.class字节码

从run方法的字节码上我们得出结论,Tester$1.class分明就是我们【new Thread(new Runnable())】内部的run()方法的代码,即是编译器为Runnable的实现类生成的匿名内部类。

从run中标红的代码我们看到这里创建通过Tester$1和TaskInfo的实例创建了Tester$1$1.class类的实例,并且以此实例作为参数执行ExecutorService#execute()方法。看到这里不妨做一个大胆的猜测: Tester$1$1.class是不是ExecutorService#execute()方法参数Runnable实现类的匿名内部类?


class Tester$1 implements java.lang.Runnable

  minor version: 0

  major version: 52

  flags: ACC_SUPER

Constant pool:

  #40 = Class              #41            // Tester$1$1

  #41 = Utf8               Tester$1$1

  #42 = Methodref          #40.#43        // Tester$1$1."<init>":(LTester$1;LTaskInfo;)V

  #43 = NameAndType        #7:#44         // "<init>":(LTester$1;LTaskInfo;)V

 

  public void run();

    descriptor: ()V

    flags: ACC_PUBLIC

    Code:

      stack=5, locals=4, args_size=1

         0: new           #17                 // class TaskInfo

         3: dup

         4: invokespecial #19                 // Method TaskInfo."<init>":()V

         7: astore_1

         8: invokestatic  #20                 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;

        11: invokevirtual #26                 // Method java/lang/Thread.getId:()J

        14: lstore_2

        15: aload_1

        16: lload_2

        17: invokevirtual #30                 // Method TaskInfo.setThreadId:(J)V

        20: invokestatic  #34                 // Method Tester.access$0:()Ljava/util/concurrent/ExecutorService;

        23: new           #40                 // class Tester$1$1

        26: dup

        27: aload_0

        28: aload_1

        29: invokespecial #42                 // Method Tester$1$1."<init>":(LTester$1;LTaskInfo;)V

        32: invokeinterface #45,  2           // InterfaceMethod java/util/concurrent/ExecutorService.execute:(Ljava/lang/Runnable;)V

        37: getstatic     #51                 // Field java/lang/System.err:Ljava/io/PrintStream;

        40: new           #57                 // class java/lang/StringBuilder

        43: dup

        44: ldc           #59                 // String in thread end id=

        46: invokespecial #61                 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V

        49: aload_1

        50: invokevirtual #64                 // Method TaskInfo.getThreadId:()J

        53: invokevirtual #67                 // Method java/lang/StringBuilder.append:(J)Ljava/lang/StringBuilder;

        56: invokevirtual #71                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;

        59: invokevirtual #75                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V

        62: return

 

 

4)   Tester$1$1.class字节码

从Run方法的字节码上,我们可以认定这就是ExecutorService#execute()方法参数Runnable实现类的匿名内部类。

从Tester$1$1(Tester$1, TaskInfo)可以看出,java编译器在生成字节码的时候,就检测了Tester$1$1中使用到的变量,然后根据这些对象构造了一个匿名实例对象。

讨论到这里基本上已经说明TaskInfo的实例是如何到ExecutorService#execute中去了,即:以异步线程中使用到的参数(taskInfo)构建了Tester$1$1的实例,所以在Tester$1$1执行的整个期间,都可以访问taskInfo。

这个问题弄清楚了,那么在ExecutorService#execute中休眠多久后再执行【long tid = task.getThreadId()】就没有任何差别了。

因为被Tester$1$1实例使用了,所以只有Tester$1#run()的run()方法执行完毕,Tester$1$1#run()方法执行完毕,TaskInfo的实例才会被回收。

 

class Tester$1$1 implements java.lang.Runnable

 

  Tester$1$1(Tester$1, TaskInfo);

    descriptor: (LTester$1;LTaskInfo;)V

    flags:

    Code:

      stack=2, locals=3, args_size=3

         0: aload_0

         1: aload_1

         2: putfield      #14                 // Field this$1:LTester$1;

         5: aload_0

         6: aload_2

         7: putfield      #16                 // Field val$task:LTaskInfo;

        10: aload_0

        11: invokespecial #18                 // Method java/lang/Object."<init>":()V

 

  public void run();

    descriptor: ()V

    flags: ACC_PUBLIC

    Code:

      stack=4, locals=3, args_size=1

         0: ldc2_w        #26                 // long 1000l

         3: invokestatic  #28                 // Method java/lang/Thread.sleep:(J)V

         6: goto          14

         9: astore_1

        10: aload_1

        11: invokevirtual #34                 // Method java/lang/InterruptedException.printStackTrace:()V

        14: aload_0

        15: getfield      #16                 // Field val$task:LTaskInfo;

        18: invokevirtual #39                 // Method TaskInfo.getThreadId:()J

        21: lstore_1

        22: getstatic     #45                 // Field java/lang/System.err:Ljava/io/PrintStream;

        25: new           #51                 // class java/lang/StringBuilder

        28: dup

        29: ldc           #53                 // String in ExecutorService thread id=

        31: invokespecial #55                 // Method java/lang/StringBuilder."<init>":(Ljava/lang/String;)V

        34: lload_1

        35: invokevirtual #58                 // Method java/lang/StringBuilder.append:(J)Ljava/lang/StringBuilder;

        38: invokevirtual #62                 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;

        41: invokevirtual #66                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V

        44: return

 

2  如果TaskInfo被循环使用,那么是否会有线程安全问题?

76d0eb7a277595adc62dc69f455ac930ed151435

在实际业务中,有很多场景因为创建Task的成本考虑,如果Task的创建成本较高,则会选择重复利用Task,即使用前从池子中取一个,使用后清理数据后放回池子中。也就是上图展示的模型。常见的例子如db连接池。

因为Task Pool被多个线程共享,所以有线程安全问题,这个需要特别注意。另外“执行任务”环节如果存在异步逻辑,也需要特别注意,否则如果遇到task中数据清理了,但是异步逻辑执行时有来取数据,将会出现问题。

 

 总结

当我们使用匿名内部类时,编译器会生成匿名内部类的单独的字节码文件,可以认为是一个全新的类。注意:在为这个类生成字节码前,会探测在匿名方法中使用到了那些变量,将他们作为参数来创建这个匿名内部类。

在代码执行过程中,ClassLoader加载的是这些编译器自动生成的匿名内部类的字节码。

 

 

 

 

相关文章
|
15天前
匿名内部类还有以下特点:
匿名内部类必须继承一个抽象类或者实现一个接口。 匿名内部类不能定义任何静态成员和静态方法。 当所在的方法的形参需要被匿名内部类使用时,必须声明为 final。 匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。
|
4月前
|
Java
成员内部类 | 静态内部类 | 局部内部类 | 匿名内部类
这篇文章详细介绍了Java中的四种内部类:成员内部类、静态内部类、局部内部类和匿名内部类,包括它们的使用场景、特点和示例代码。
成员内部类 | 静态内部类 | 局部内部类 | 匿名内部类
|
6月前
|
Java
【Java基础】 内部类 (成员内部类、局部内部类、静态内部类、匿名内部类)
Java内部类 (成员内部类、局部内部类、静态内部类、匿名内部类)
36 0
|
Java
内部类(下)匿名内部类,静态内部类的使用
内部类(下)匿名内部类,静态内部类的使用
84 0
|
7月前
|
Java
匿名内部类&Lambda表达式&函数式接口
匿名内部类&Lambda表达式&函数式接口
37 0
内部类的概念与分类(成员内部类,局部内部类,匿名内部类)
内部类,就是一个类内部包含另一个类,即一个事物的内部包含着另一个事物。例如:身体和心脏 、汽车与发动机之间的关系。 可以看见在out下的内部类文件命名规则是 外部类$内部类.class类名称 对象名 = new 类名称();外部类名称.内部类名称 对象名 = new 外部类名称().new 外部类名称(); 把这条公式插入到demo07InnerClass 中 使用heart.调用内部类方法 如果一个类是定义在一个方法内部的,那么这是一个局部内
42 2
成员内部类、静态内部类、局部内部类、匿名内部类的精髓与应用
成员内部类、静态内部类、局部内部类、匿名内部类的精髓与应用
115 0
|
Java
内部类(上)成员内部类,局部内部类的使用
内部类(上)成员内部类,局部内部类的使用
62 0
|
Java
3.4 内部类的类型:匿名内部类
3.4 内部类的类型:匿名内部类
72 0