【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加载的是这些编译器自动生成的匿名内部类的字节码。


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

查看所有标签

猜你喜欢:

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

产品经理全栈运营实战笔记

产品经理全栈运营实战笔记

林俊宇 / 化学工业出版社 / 49.8元

本书凝结作者多年的产品运营经验,读者会看到很多创业公司做运营的经验,书中列举了几十个互联网产品的运营案例去解析如何真正做好一个产品的冷启动到发展期再到平稳期。本书主要分为六篇:互联网运营的全面貌;我的运营生涯;后产品时代的运营之道;揭秘刷屏事件的背后运营;技能学习;深度思考。本书有很多关于产品运营的基础知识,会帮助你做好、做透。而且将理论和作者自己的案例以及其他人的运营案例结合起来,会让读者更容易......一起来看看 《产品经理全栈运营实战笔记》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

在线进制转换器
在线进制转换器

各进制数互转换器

正则表达式在线测试
正则表达式在线测试

正则表达式在线测试