本博客是由工作中遇到的一个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被循环使用,那么是否会有线程安全问题?
在实际业务中,有很多场景因为创建Task的成本考虑,如果Task的创建成本较高,则会选择重复利用Task,即使用前从池子中取一个,使用后清理数据后放回池子中。也就是上图展示的模型。常见的例子如db连接池。
因为Task Pool被多个线程共享,所以有线程安全问题,这个需要特别注意。另外“执行任务”环节如果存在异步逻辑,也需要特别注意,否则如果遇到task中数据清理了,但是异步逻辑执行时有来取数据,将会出现问题。
四 总结
当我们使用匿名内部类时,编译器会生成匿名内部类的单独的字节码文件,可以认为是一个全新的类。注意:在为这个类生成字节码前,会探测在匿名方法中使用到了那些变量,将他们作为参数来创建这个匿名内部类。
在代码执行过程中,ClassLoader加载的是这些编译器自动生成的匿名内部类的字节码。