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

栏目: Java · 发布时间: 6年前

内容简介:【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被循环使用,那么是否会有线程安全问题?

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

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

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

  总结

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

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


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

数据结构C++语言描述

数据结构C++语言描述

William Ford,William Topp / 刘卫东 沈官林 / 清华大学出版社 / 1999-09-01 / 58.00

一起来看看 《数据结构C++语言描述》 这本书的介绍吧!

SHA 加密
SHA 加密

SHA 加密工具

XML 在线格式化
XML 在线格式化

在线 XML 格式化压缩工具