🦊 Java
1、Java基础
1.1 StringBuffer和StringBuilder的区别
1.2 ConcurrentHashMap和TreeMap实现原理
1.3 ArrayList和LinkedList实现原理
1.4 HashSet和TreeSet实现原理
1.5 深拷贝与浅拷贝
1.6 抽象类与接口
2、Java多线程
2.1 并发编程的三大特性
2.2 指令重排
2.3 Volatile原理
2.4 CAS原理
2.5 Java的4种引用级别
2.6 Java中的锁
2.7 Synchronized实现原理
2.8 线程池实现原理
2.9 AQS
2.10 创建线程的方式
2.11 ThreadLocal原理
3、JVM
3.1 判断对象是否存活的方法
3.2 JVM内存结构
3.3 常见的垃圾收集算法有哪些
3.4 指针碰撞和空闲列表
3.5 常见的垃圾收集器有哪些
3.6 内存溢出与内存泄漏的区别
3.7 常用的JVM启动参数有哪些
3.8 反射机制
4、NIO
4.1 概述
5、Spring
5.1 Spring IOC
5.2 Spring AOP
6、SpringBoot
6.1 SpringBoot、SpringCloud的联系与区别
-
+
游客
注册
登录
创建线程的方式
## 继承 Thread 类 通过继承 Thread 类,并重写 `run` 方法,就可以创建一个线程。 - 首先定义一个类来继承 Thread 类,重写 `run` 方法。 - 然后创建这个子类的对象,并调用 `start` 方法启动线程。 ~~~java public class MyThread extends Thread{ @Override public void run() { System.out.println("Thread run..."); } public static void main(String[] args) { MyThread thread = new MyThread(); thread.start(); } } ~~~ 这种方式编写简单,如果需要访问当前线程,无需使用 `Thread.currentThread()` 方法,**直接使用** `this`,即可获得当前线程。但是因为线程类已经继承了 Thread 类,所以**不能再继承其他的父类**。 > Java 中为什么要单继承,多实现? > > 要解决这个问题,我们首先需要明确为什么类只能单继承: > > - 如果子类继承的多个父类里面有相同的方法或者属性,子类将**不知道具体继承哪一个**,会产生歧义。例如 A 同时继承了 B、C,B 和 C 有相同的方法 d ,那么 A 该继承哪个类的 d 方法呢,这是不明确的。 > - 同样,如果与父类中的方法同名,子类中没有覆盖,同样会产生上面的错误。 > > 基于以上原因,类中只能单继承。下面我们讨论一下为什么类可以多实现。接口之所以可以多继承,是因为 [接口可以避免上述问题](https://notebook.ricear.com/doc/875): > > - 接口里面定义的都是静态常量,方法都是抽象方法,**没有逻辑实现**。具体的方法必须由实现接口的类覆盖实现,在调用时**只会调用实现类的方法**,不会出现歧义。 > > - 接口中的变量都是**静态成员常量**(`public static final`),会在**编译期就感知到错误**,即使存在一定的引用不明确冲突,也会在编译时提示错误,因此也不会导致歧义。 > > - 如果子接口继承的多个父接口中有相同的属性 `a`,那么类在实现接口时是不能调用接口里的属性 `a` 的,与类不能多继承原因一致,造成引用不明确。如果是不同的属性,实现类是可以调用的。即接口可以继承多个父接口不同的属性,**不能继承相同的属性**。 > > ![image-20230419113421361](https://notebook.ricear.com/media/202304/2023-04-19_113438_3041740.5892972062289332.png) > > 综上所述,Java 中类只能单继承,是因为在父类存在多个相同的方法时子类不知道具体继承哪一个,会产生歧义。接口中的变量都是静态成员常量,方法都是抽象方法,没有逻辑实现,具体的实现须由子类覆盖实现,调用时只会调用实现类的方法,不会出现歧义,所以接口可以多实现。 > 多线程启动的原理是什么? > > 第一步:通过 `threadStatus` 判断当前线程的状态,如果不是 0,即不是 NEW 的话,则抛出一个 `IllegalThreadStateException`,这也意味着**一个线程只能启动一次**。 > > ```java > if (threadStatus != 0) > throw new IllegalThreadStateException(); > ``` > > 第二步:通知线程组该线程即将启动,同时**将当前线程添加到线程组的列表中**,并**将线程组中未启动的线程数减 1**。 > > ```java > group.add(this); > ``` > > ~~~java > void add(Thread t) { > synchronized (this) { > if (destroyed) { > throw new IllegalThreadStateException(); > } > if (threads == null) { > threads = new Thread[4]; > } else if (nthreads == threads.length) { > threads = Arrays.copyOf(threads, nthreads * 2); > } > threads[nthreads] = t; > > // This is done last so it doesn't matter in case the > // thread is killed > nthreads++; > > // The thread is now a fully fledged member of the group, even > // though it may, or may not, have been started yet. It will prevent > // the group from being destroyed so the unstarted Threads count is > // decremented. > nUnstartedThreads--; > } > } > ~~~ > > 第三步:调用 native 方法 `start0()` 底层**开启异步线程**,并调用 `run` 方法。 > > ~~~java > private native void start0(); > ~~~ > > 在具体介绍 `start0()` 方法之前,我们先解释一下什么是 native 方法。我们知道 Java 是一个跨平台的语言,用 Java 编译的代码可以运行在任何安装了 JVM 的系统上。然而各个系统的底层实现是有区别的,为了使 Java可以跨平台,于是 JVM 提供了 Java Native Interface 机制,即 JNI,Java 本地接口。当需要使用到一些系统方法时,由 JVM帮我们去调用系统底层,而 Java 本身只需要告知 JVM 要做的事情,即调用某个 native 方法即可。例如,当我们需要启动一个线程时,无论在哪个平台,我们调用的都是 `start0() `方法,由 JVM 根据不同的操作系统,去调用相应系统底层方法,帮我们真正地启动一个线程。 > > 下面我们以一个具体的示例来展示一下 JNI 的具体使用方法,以便我们下面更好的理解 start0() 方法的调用原理。 > > - 在 Java 中定义 native 方法。 > > ~~~java > public class JNI { > public native void hello(); > > public static void main(String[] args) { > JNI jni = new JNI(); > jni.hello(); > } > } > ~~~ > > - 生成相应的 `.h` 头文件(即接口)。 > > 命令为 `javac -h . JNI.java`,命令执行完成后会生成两个新文件,分别是 `JNI.class`,即 JNI 类的字节码;`com_ricear_multi_thread_JNI.h`,即 `.h` 头文件,这个文件是 C 和 C++ 中所需要用到的,这个文件定义了方法的参数、返回类型等,但不包含实现,类似于 Java 中的接口,而 Java 代码正是通过这个“接口“找到真正需要执行的方法。 > > ~~~c++ > /* DO NOT EDIT THIS FILE - it is machine generated */ > #include <jni.h> > /* Header for class com_ricear_multi_thread_JNI */ > > #ifndef _Included_com_ricear_multi_thread_JNI > #define _Included_com_ricear_multi_thread_JNI > #ifdef __cplusplus > extern "C" { > #endif > /* > * Class: com_ricear_multi_thread_JNI > * Method: hello > * Signature: ()V > */ > JNIEXPORT void JNICALL Java_com_ricear_multi_1thread_JNI_hello > (JNIEnv *, jobject); > > #ifdef __cplusplus > } > #endif > #endif > ~~~ > > - 编写相应的 `.c` 或 `.cpp` 文件(即实现)。 > > 我们希望该方法简单地输出一个 `hello jni`,于是定义如下方法,并将其保存在 `com_ricear_multi_thread_JNI.c` 文件中。 > > ~~~c++ > #include "com_ricear_multi_thread_JNI.h" > > JNIEXPORT void JNICALL Java_com_ricear_multi_1thread_JNI_hello(JNIEnv *, jobject) { > printf("hello jni\n"); > } > ~~~ > > - 将接口和实现链接到一起,生成动态链接库。 > > ~~~shell > MAC: gcc -dynamiclib -I /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/include com_ricear_multi_thread_JNI.c -o libJNI.jnilib > > Linux: gcc -shared -I /usr/lib/jdk1.8.0_181/include com_ricear_multi_thread_JNI.c -o libJNI.so > ~~~ > > > MAC 执行上面的命令时可能会提示 `jni_md.h' file not found`,这时需要把 `/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/include/darwin` 目录中的 `jni_md.h` 复制到 `/Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/include` 目录即可。 > > 此时在目录下就会多出一个 `libJNI.jnilib` 或 `libJNI.so` 的动态链接库。 > > - 在 Java 中引入改库,即可调用 native 方法。 > > ~~~java > public class JNI { > static { > System.setProperty("java.library.path", "."); // 设置查找路径为当前项目路径 > System.loadLibrary("JNI"); // 加载动态库的名称 > } > public native void hello(); > > public static void main(String[] args) { > JNI jni = new JNI(); > jni.hello(); > } > } > ~~~ > > 重新编译 `.class` 文件,命令为 `javac -h . JNI.java`。然后将编译后的 `.class` 文件放到 `.com/ricear/multi_thread` 目录下(包名是啥,目录就是啥),然后执行即可,命令为 `java com.ricear.multi_thread.JNI`。最后输出结果如下。 > > ``` > hello jni > ``` > > > 以上就是 JNI 的基本使用方法。在了解了JNI 的基本使用方法之后,下面我们回到 Thread 的 `start0` 方法,看看 JVM 在幕后为我们做了什么。下面的代码分析基于 [openjdk](https://openjdk.org),版本为 [jdk8](https://hg.openjdk.org/jdk8/jdk8/jdk)(点击页面右侧的 zip 按钮即可下载)。 > > Thread 类在实现 JNI 的时候并非是将每一个 native 方法都直接定义在自己的头文件中,而是通过一个 `registerNatives` 方法动态注册(对应的正是 `Thread.java` 中的 `registerNatives` 方法),注册所需要的信息都被定义在了 `methods` 数组中,包括方法名、方法签名和接口方法;接口方法的定义被统一放到了 `jvm.h` 中。这时 JNI 接口方法的名字就不再受到固定格式限制了。 > > ~~~c++ > #include "jni.h" > #include "jvm.h" > > #include "java_lang_Thread.h" > > #define THD "Ljava/lang/Thread;" > #define OBJ "Ljava/lang/Object;" > #define STE "Ljava/lang/StackTraceElement;" > #define STR "Ljava/lang/String;" > > #define ARRAY_LENGTH(a) (sizeof(a)/sizeof(a[0])) > > static JNINativeMethod methods[] = { > {"start0", "()V", (void *)&JVM_StartThread}, > {"stop0", "(" OBJ ")V", (void *)&JVM_StopThread}, > {"isAlive", "()Z", (void *)&JVM_IsThreadAlive}, > {"suspend0", "()V", (void *)&JVM_SuspendThread}, > {"resume0", "()V", (void *)&JVM_ResumeThread}, > {"setPriority0", "(I)V", (void *)&JVM_SetThreadPriority}, > {"yield", "()V", (void *)&JVM_Yield}, > {"sleep", "(J)V", (void *)&JVM_Sleep}, > {"currentThread", "()" THD, (void *)&JVM_CurrentThread}, > {"countStackFrames", "()I", (void *)&JVM_CountStackFrames}, > {"interrupt0", "()V", (void *)&JVM_Interrupt}, > {"isInterrupted", "(Z)Z", (void *)&JVM_IsInterrupted}, > {"holdsLock", "(" OBJ ")Z", (void *)&JVM_HoldsLock}, > {"getThreads", "()[" THD, (void *)&JVM_GetAllThreads}, > {"dumpThreads", "([" THD ")[[" STE, (void *)&JVM_DumpThreads}, > {"setNativeName", "(" STR ")V", (void *)&JVM_SetNativeThreadName}, > }; > > #undef THD > #undef OBJ > #undef STE > #undef STR > > JNIEXPORT void JNICALL > Java_java_lang_Thread_registerNatives(JNIEnv *env, jclass cls) > { > (*env)->RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods)); > } > ~~~ > > ~~~java > private static native void registerNatives(); > ~~~ > > 下面我们一起探究 `start0()` 方法的具体调用过程(`start0()` 和 `run()` 方法的调用过程如下图所示): > > ![image-20230523215845399](https://notebook.ricear.com/media/202305/2023-05-23_215820_4864840.7774784670735261.png) > > - 既然 `start0()` 方法的接口方法被定义在了 `jvm.h` 中,那么我们先查看 `jvm.h`,就可以找到 `JVM_StartThread` 的定义了。 > > ~~~c++ > JNIEXPORT void JNICALL > JVM_StartThread(JNIEnv *env, jobject thread); > ~~~ > > - 接下来我们查看 `jvm.cpp`(`hotspot` 目录 `src/share/vm/prims`),这里能看见 `JVM_StartThread` 的具体实现,关键点是通过创建一个 JavaThread 类来**创建 C++ 级别的线程**。 > > ~~~c++ > JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread)) > JVMWrapper("JVM_StartThread"); > JavaThread *native_thread = NULL; > bool throw_illegal_thread_state = false; > > { > ... > /** > * 创建一个 C++ 级别的线程 > */ > native_thread = new JavaThread(&thread_entry, sz); > ... > } > ... > JVM_END > ~~~ > > - 在 `thread.cpp` 中,可以看到 `JavaThread` 的构造函数,其中创建了一个系统线程。 > > ~~~c++ > JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) : > Thread() > { > ... > /** > * 创建系统线程 > */ > os::create_thread(this, thr_type, stack_sz); > } > ~~~ > > - 在 `hotspot` 源码目录下找到不同系统创建线程的方法,这里以 Linux 为例,在 `os_linux.cpp` 中找到 `create_thread` 的方法。 > > ~~~c++ > bool os::create_thread(Thread* thread, ThreadType thr_type, size_t stack_size) { > ... > pthread_t tid; > int ret = pthread_create(&tid, &attr, (void* (*)(void*)) java_start, thread); > ... > } > ~~~ > > 这个 `pthread_create` 方法就是**最终创建系统线程的底层方法**。因此 Java 线程 `start` 方法的本质其实就是**通过 JNI 机制**,**最终调用系统底层的 pthread_create 方法**,**创建了一个系统线程**,因此 Java **线程和系统线程是一个一对一的关系**。 > > 但是到这里我们的探究并没有结束,在 Java 的 Thread 类中,我们会传入一个执行我们指定任务的 Runnable 对象,在 Thread 的 `run()` 方法中调用。那么当 Java 通过 JNI 调用到 `pthread_create` 创建完系统线程后,又要如何回调 Java 中的 `run` 方法呢?前面的探究我们是从 Java 层开始,从上往下找,此时我们要反过来,从下往上找了。 > > - `pthread_create`。先看 `pthread_create` 方法本身,它接收 4 个参数,其中第三个参数 `start_routine` 是**系统线程创建后需要执行的方法**,第四个参数 `arg` 是 `start_routine` 方法需要的参数。 > > ~~~c++ > pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void *), void *arg); > ~~~ > > - `os_linux.cpp`。查看 `create_thread` 方法中调用 `pthread_create` 的代码,可以看到 `java_start` 就是系统线程所执行的方法,而 Thread 则是传递给 `java_start` 的参数。 > > ~~~c++ > int ret = pthread_create(&tid, &attr, (void* (*)(void*)) java_start, thread); > ~~~ > > 查看 `java_start` 方法,它获取的参数正是一个 Thread,并调用其 `run()` 方法。注意这个 Thread 是 C++ 级别的线程,来自于 `pthread_create` 的第四个参数。 > > ~~~c++ > static void *java_start(Thread *thread) { > ... > // call one more level start routine > thread->run(); > ... > return 0; > } > ~~~ > > - `thread.cpp`。查看 `JavaThread::run()` 方法,其主要的执行内容在 `thread_main_inner` 方法中。 > > ~~~c++ > void JavaThread::run() { > ... > /** > * 主要的执行内容 > */ > thread_main_inner(); > ... > } > ~~~ > > 查看 `JavaThread::thread_main_inner()` 方法,其内部通过 `entry_point` 回调。 > > ~~~c++ > void JavaThread::thread_main_inner() { > ... > /** > * 调用 entry_point,执行外部传入的方法,这里的参数 this > * 指的是JavaThread 对象本身,后面会看到该方法的定义 > */ > this->entry_point()(this, this); > ... > } > ~~~ > > 查看 `JavaThread::JavaThread` 构造函数,可以看到这里的 `entry_point` 是外部传入的。 > > ~~~c++ > JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) : > Thread() > { > ... > set_entry_point(entry_point); > ... > } > ~~~ > > - `jvm.cpp`。查看 `JVM_StartThread` 方法,可以看到传给 `JavaThread` 的 `entry_point` 是 `thread_entry`。 > > ~~~c++ > JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread)) > JVMWrapper("JVM_StartThread"); > JavaThread *native_thread = NULL; > bool throw_illegal_thread_state = false; > { > ... > /** > * 传给构造函数的 entry_point 是 thread_entry > */ > native_thread = new JavaThread(&thread_entry, sz); > ... > } > ... > JVM_END > ~~~ > > 查看 `thread_entry`,其中调用了 `JavaCalls::call_virtual` 去回调 Java 级别的方法。 > > ~~~c++ > static void thread_entry(JavaThread* thread, TRAPS) { > HandleMark hm(THREAD); > Handle obj(THREAD, thread->threadObj()); // obj 正是根据 thread 对象获取到的,JavaThread 在调用时会传入 this > JavaValue result(T_VOID); // 返回结果为空 > /** 回调 Java 级别的方法 **/ > JavaCalls::call_virtual(&result, // 返回对象 > obj, // 实例对象 > KlassHandle(THREAD, SystemDictionary::Thread_klass()), // 类 > vmSymbols::run_method_name(), // 方法名 > vmSymbols::void_method_signature(), // 方法签名 > THREAD); > } > ~~~ > > - `vmSymbols.hpp`(`hotspot` 目录 `src/share/vm/classfile`)。我们查看获取方法名(`run_method_name`)和方法签名(`void_method_signature`),可以看到正是获取一个方法名为 `run`,且不获取任何参数,返回值为 `void` 的方法。 > > ~~~c++ > template(run_method_name, "run") > ... > template(void_method_signature, "()V") > ~~~ > > 于是系统线程就能成功地回调 Java 级别的 `run` 方法了。 > > 下面我们可以根据上面的原理自己实现一个简单的回调方法: > > - 修改 `JNI.java` 并重新命名为 `JNI_v2.java`。 > > ~~~java > static { > System.setProperty("java.library.path", "."); // 设置查找路径为当前项目路径 > System.loadLibrary("JNI_v2"); // 加载动态库的名称 > } > public native void hello(); > > /** > * 新增一个回调方法 > */ > public void callback() { > System.out.println("this is a callback"); > } > > public static void main(String[] args) { > JNI_v2 jni = new JNI_v2(); > jni.hello(); > } > ~~~ > > - 使用命令 `javac -h . JNI_v2.java` 生成 `JNI_v2.h`。然后修改 `com_ricear_multi_thread_JNI.c` 文件并重新命名为 `com_ricear_multi_thread_JNI_v2.c`。 > > ~~~c++ > #include "com_ricear_multi_thread_JNI_v2.h" > > JNIEXPORT void JNICALL Java_com_ricear_multi_1thread_JNI_1v2_hello(JNIEnv *env, jobject obj) { > jclass clazz = (*env)->GetObjectClass(env, obj); // 获取类信息 > jmethodID methodId = (*env)->GetMethodID(env, clazz, "callback", "()V"); // 根据方法名和签名获取方法的 id > (*env)->CallVoidMethod(env, obj, methodId); // 调用方法 > } > ~~~ > > - 重新生成动态链接库,运行。 > > ~~~shell > gcc -dynamiclib -I /Library/Java/JavaVirtualMachines/jdk1.8.0_181.jdk/Contents/Home/include com_ricear_multi_thread_JNI_v2.c -o libJNI_v2.jnilib > java com.ricear.multi_thread.JNI_v2 > ~~~ > > - 成功的到输出结果。 > > ~~~ > this is a callback > ~~~ > > 上面的回调只是最基本的使用,而 JVM 中的官方回调方法,涉及到 Java 的父类继承关系、方法句柄、vtable 等内容,这里也就不展开了,有兴趣的同学可以自己研究。 ## 实现 Runnable 接口 通过实现 Runnable 接口创建的启动线程的一般步骤如下: - 定义 Runnable 接口的实现类。这个步骤一样要重写 `run()` 方法,和 Thread 一样,这里的 `run()` 方法也是**线程的执行体**。 - 创建 Runnable 实现类的实例,并用这个实例作为 Thread 的 `target` 来创建 Thread 对象,这个 Thread 对象才是**真正的线程对象**。 - 通过调用线程对象的 `start()` 方法来启动线程。 ~~~java public class MyThread2 implements Runnable{ @Override public void run() { System.out.println("Runnable thread run..."); } public static void main(String[] args) { MyThread2 runnable = new MyThread2(); Thread thread = new Thread(runnable); thread.start(); } } ~~~ 这种方式相比于继承 Thread 类,具有如下优点: - Java 是单继承,多实现的。因此实现 Runnable 接口的方法还能继承其他的类,便于后续代码维护。 - 直接继承 Thread 类的方式不适合资源共享。但是实现 Runnable 接口的方式可以很容易的实现资源共享,适合多个相同线程处理同一份资源的情况。 > 为什么继承 Thread 类创建线程的方式不适合资源共享,但是实现 Runnable 接口的方式却可以? > > 假如有一个任务:两个线程共执行 20 次,每次执行完打印剩余需要执行的次数。 > > 使用继承 Thread 类在实现的时候会出现线程之间变量不能共享,导致每次都是各自的变量和结果。 > > ~~~java > public class MyThread_v2 extends Thread{ > int count = 20; > String name; > public MyThread_v2(String name) { > this.name = name; > } > > @Override > public void run() { > while (count > 0) { > System.out.println(name + ": " + --count); > } > } > > public static void main(String[] args) { > MyThread_v2 thread1 = new MyThread_v2("Thread1"); > MyThread_v2 thread2 = new MyThread_v2("Thread2"); > thread1.start(); > thread2.start(); > } > } > ~~~ > > ~~~ > Thread2: 19 > Thread1: 19 > Thread1: 18 > Thread1: 17 > Thread1: 16 > Thread1: 15 > Thread2: 18 > Thread2: 17 > Thread2: 16 > Thread1: 14 > Thread1: 13 > Thread1: 12 > Thread1: 11 > Thread1: 10 > Thread1: 9 > Thread1: 8 > Thread1: 7 > Thread1: 6 > Thread1: 5 > Thread1: 4 > Thread1: 3 > Thread1: 2 > Thread2: 15 > Thread2: 14 > Thread2: 13 > Thread2: 12 > Thread2: 11 > Thread2: 10 > Thread2: 9 > Thread2: 8 > Thread2: 7 > Thread2: 6 > Thread2: 5 > Thread2: 4 > Thread1: 1 > Thread1: 0 > Thread2: 3 > Thread2: 2 > Thread2: 1 > Thread2: 0 > ~~~ > > 使用实现 Runnable 接口的方式可以在不同线程之间共享变量,从而可以很好的避免上面的问题。 > > ~~~java > public class MyThread2_v2 implements Runnable{ > int count = 20; > > @Override > public void run() { > while (count > 0) { > System.out.println(Thread.currentThread().getName() + ": " + --count); > } > } > > public static void main(String[] args) { > MyThread2_v2 runnable = new MyThread2_v2(); > Thread thread1 = new Thread(runnable, "Thread1"); > Thread thread2 = new Thread(runnable, "Thread2"); > thread1.start(); > thread2.start(); > } > } > ~~~ > > ~~~ > Thread1: 19 > Thread1: 17 > Thread1: 16 > Thread1: 15 > Thread1: 14 > Thread1: 13 > Thread1: 12 > Thread1: 11 > Thread1: 10 > Thread1: 9 > Thread1: 8 > Thread1: 7 > Thread1: 6 > Thread1: 5 > Thread1: 4 > Thread1: 3 > Thread1: 2 > Thread1: 1 > Thread1: 0 > Thread2: 18 > ~~~ > > 因此在使用的时候更加推荐使用实现 Runnable 接口的方式创建线程。 ## 基于 Callable 和 Future 无论是继承 Thread 类还是实现 Runnable 接口的方法来创建线程都会产生一个问题,就是**在执行完任务之后无法获取执行结果**。如果需要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到效果,这样使用起来就比较麻烦。遇到这种情况时就可以考虑采用 Callable 和 Future 的方式的创建线程。 基于 Callable 和 Future 创建线程的一般步骤如下: - 创建 Callable 接口的实现类,并实现 `call()` 方法,该方法将作为线程执行体,且有返回值。 - 创建 Callable 实现类的实例,使用 FutureTask 类来包装 Callable 对象,该 FutureTask 对象封装了获取 Callable 对象的 `call()`方法返回值的 `get()` 方法。 - 使用 FutureTask 对象作为 Thread 对象的 `target` 创建并启动线程。 - 调用 FutureTask 对象的 `get()` 方法获取子线程执行结束后的返回值。 ~~~java public class MyThread3 implements Callable { @Override public Object call() throws Exception { System.out.println("call method..."); Thread.sleep(4000); return 10; } public static void main(String[] args) throws ExecutionException, InterruptedException { MyThread3 callable = new MyThread3(); FutureTask<Integer> task = new FutureTask<Integer>(callable); Thread thread = new Thread(task); thread.start(); System.out.println(task.get()); } } ~~~ 在介绍完基于 Callable 和 Future 创建线程的一般步骤后,下面就让我们一起探究一下内部的实现原理。 ### Callable Callable 接口代表一段**可以调用并返回结果的代码**,**使用泛型来定义它的返回类型**。 ~~~java public interface Callable<V> { V call() throws Exception; } ~~~ 一般情况下 Callable 是配合 ExecutorService 来使用的,在 ExecutorService 接口中生命了若干个 `submit()` 方法的重载版本。 ~~~java <T> Future<T> submit(Callable<T> task); <T> Future<T> submit(Runnable task, T result); Future<?> submit(Runnable task); ~~~ 一般情况下我们使用第一个和第三个 `submit()` 方法,第二个 `submit()` 方法很少使用。 > Callable 和 Runnable 有什么区别? > > - Callable 规定的方法是 `call()`,Runnable 固定的方法是 `run()`。 > - Callable 的任务执行后可返回值,而 Runnable 的任务是不能返回值的。 > - `call()` 方法可以抛出异常,`run()` 方法不可以。因为 `run()` 方法本身没有抛出异常,所以自定义的线程类在重写 `run()` 方法的时候也无法抛出异常。 > - 运行 Callable 的任务可以拿到一个 Future 对象。通过 Future 对象可以了解任务的执行情况,可以取消任务的执行,还可以获取任务的执行结果。 ### Future Future 接口表示**异步任务**,是**还没有完成的任务给出的未来结果**。所以说 Callable 用于**产生结果**,Future 用于**获取结果**。当我们在 [线程池](https://notebook.ricear.com/doc/818) 通过 `Future` 可以获得 Callable 任务的状态,并通过 `get()` 方法可以等待 Callable 执行结束时获取它的执行结果。 Future 接口中声明了 5 个方法,具体如下: ```java boolean cancel(boolean mayInterruptIfRunning); boolean isCancelled(); boolean isDone(); V get() throws InterruptedException, ExecutionException; V get(long timeout, TimeUnit unit) throws InterruptedException, ExecutionException, TimeoutException; ``` 以上方法有两个需要注意的地方: - `cancel()` 用来取消任务,如果取消任务成功,则返回 true,否则返回 false。参数 mayInterruptIfRunning 表示是否允许取消正在执行却没有执行完毕的任务。如果任务还没有执行,则返回 true; 如果任务已经执行完成,则返回 false。如果任务正在运行,当该参数设置为 true 时,则返回 true,否则返回 false。 - `get()` 用来获取执行结果,这个方法会**一直阻塞**,等待任务执行完毕才返回。 - `get(long timeout, TimeUnit unit)` 和 `get()` 类似,不同的是该方法有一个超时时间,如果在指定时间内还没获取到结果,就直接返回 `null`。 Future 只是一个接口,定义了最基本的一些任务操作和状态判断。FutureTask 是对 Future 和 Runnable 一种最简单的实现(FutureTask 实现了 RunnableFuture 接口,而 RunnableFuture 继承自 Runnable 和 Future,因此既可以被线程异步执行,又可以获取返回结果,解决了 Runnable 异步任务没有返回结果的缺陷),下面我们将从 FutureTask 入手去了解 Future 的内部机制。 ~~~java public class FutureTask<V> implements RunnableFuture<V> {} ~~~ ~~~java public interface RunnableFuture<V> extends Runnable, Future<V> { void run(); } ~~~ ### FutureTask #### 核心属性 FutureTask 包含以下几个核心属性: ~~~java private volatile int state; private Callable<V> callable; private Object outcome; private volatile Thread runner; private volatile WaitNode waiters; ~~~ - state:**状态**。 - Callable:内部封装的 Callable 对象。如果通过构造函数传的是 Runnable 对象,FutureTask 会通过调用 `Executors#callable`,把 Runnable 对象封装成一个 Callable。 - outcome:用于 **保存计算结果或者异常信息**。 - runner:用来 **运行 Callable 的线程**。 - waiters:用来 **保存等待的线程**。 ##### 状态 FutureTask 定义了 7 种状态,由 state 属性来表示,**采用 [volatile](https://notebook.ricear.com/doc/528) 来进行修饰**,**确保了不同线程线程对它修改的可见性**,也就是说只要有任何一个线程修改了这个变量,那么其他所有的线程都会知道最新的值。 ~~~java private volatile int state; private static final int NEW = 0; private static final int COMPLETING = 1; private static final int NORMAL = 2; private static final int EXCEPTIONAL = 3; private static final int CANCELLED = 4; private static final int INTERRUPTING = 5; private static final int INTERRUPTED = 6; ~~~ - **NEW**:初始状态,FutureTask **刚被创建**,**正在计算中** 都是该状态。 - **COMPLETING**:中间状态,表示 **计算已完成正在对结果进行赋值** 或 **正在处理异常**。 - **NORMAL**:终止状态,表示**计算已完成**,**结果已经被赋值**。 - **EXCEPTIONAL**:终止状态,表示计算过程**已经被异常打断**。 - **CANCELLED**:终止状态,表示计算过程**已经被 cancel 操作终止**(一般出现在任务还没开始执行或者已经开始执行但是还没有执行完成的时候,用户调用了 `cancel(false)` 的方法来取消任务且不中断任务执行线程,这个时候状态会从 NEW 转化为 CANCEL 状态)。 - **INTERRUPTTING**:中间状态,表示 **计算过程已开始并且被中断**,**正在修改状态**(一般出现在任务还没开始执行或者已经执行但是还没有执行完成的时候,用户调用了 `cancel(true)` 方法取消任务并且要中断任务执行线程但是还没有中断任务执行线程之前,状态会从 NEW 转化为 INTERRUPTTING)。 - **INTERRUPTED**:终止状态,表示 **计算过程已开始并且被中断**,**目前已完全停止**(一般出现在调用 `interrupt()` 中断任务执行线程之后状态会从 INTERRUPTTING 转化为 INTERRUPTED)。 综上所述,FutureTask 的任务状态共有 1 个初始态,2 个中间态和 4 个终止态,具体如下: - **初始态**:NEW - **中间态**:COMPLETING、INTERRUPTTING - **完成态**:NORMAL、EXCEPTIONAL、CANCELLED、INTERRUPTED 虽说状态有那么多,但是状态的转换路径却只有 4 种,如下图所示。 ![state of FutureTask](https://notebook.ricear.com/media/202305/2023-05-25_165300_0983210.7575185587226567.png) > 任务的中间状态是一个瞬态,非常的短暂。而且任务的中间态并不代表任务正在执行,而是 **任务已经执行完了**,**正在设置最终的返回结果**。因此,可以这么说【**只要 state 不处于 NEW 状态**,**就说明任务已经执行完毕**】。 > > 这里的执行完毕是指传入的 Callable 对象的 `call()` 方法执行完毕,或者跑出了异常。所以这里的 COMPLETING 的名字就显得有点迷惑性,它并不意味着任务正在执行,而意味着 `call()` 方法 **已经执行完毕**,**正在设置任务的执行结果**。 ##### 队列 我们知道 FutureTask 实现了 Future 接口,可以获取任务的执行结果。如果获取结果时任务还没有执行完成,那么获取结果的线程就会在一个 **等待队列** 中挂起,直到任务执行完毕被唤醒。FutureTask 中的队列实现是一个单向链表,它表示 **所有等待任务执行完毕的线程的集合**。每一个节点的结构如下所示。 ~~~java static final class WaitNode { volatile Thread thread; volatile WaitNode next; WaitNode() { thread = Thread.currentThread(); } } ~~~ 节点的属性比较简单,只包含了一个记录线程的 Thread 属性和指向下一个节点的 `next` 属性。值得一提的是,FutureTask 中的这个单向链表是当做栈来使用的,确切的说是 [Treiber 栈](https://en.wikipedia.org/wiki/Treiber_stack)。它是一种 **线程安全** 的栈,使用 [CAS](https://notebook.ricear.com/doc/529) 来完成入栈和出栈操作。由于 FutureTask 中的队列本质上是一个 Treiber 栈,那么使用这个队列就只需要一个指向栈顶节点的指针就行了,在 FutureTask 中,就是 waiters 属性,它是整个单向链表的头节点。 ![Treiber stack](https://notebook.ricear.com/media/202305/2023-05-27_155617_8423830.3434012326376943.png) CAS 操作一般用来改变状态的,在 FutureTask 中也不例外,一般在静态代码快中初始化需要 CAS 操作的属性的偏移量。 ~~~java // Unsafe mechanics private static final sun.misc.Unsafe UNSAFE; private static final long stateOffset; private static final long runnerOffset; private static final long waitersOffset; static { try { UNSAFE = sun.misc.Unsafe.getUnsafe(); Class<?> k = FutureTask.class; stateOffset = UNSAFE.objectFieldOffset (k.getDeclaredField("state")); runnerOffset = UNSAFE.objectFieldOffset (k.getDeclaredField("runner")); waitersOffset = UNSAFE.objectFieldOffset (k.getDeclaredField("waiters")); } catch (Exception e) { throw new Error(e); } } ~~~ 从上面的静态代码块我们可以看出,CAS 操作主要针对 3 个属性,包括 state、runner 和 waiters,说明这 3 个属性基本是会被多个线程同时访问的。其中 state 代表了任务的状态,waiters 代表了指向栈顶节点的指针,runner 代表了执行 FutureTask 中的任务的线程。 > 之所以需要一个属性来记录执行任务的线程是为了中断或者取消任务做准备的,只有知道了执行任务的线程是谁,我们才能去中断它。 ##### 构造函数 FutureTask 的构造函数有两个,具体如下所示。 ```java public FutureTask(Callable<V> callable) { if (callable == null) throw new NullPointerException(); this.callable = callable; this.state = NEW; // ensure visibility of callable } ``` 第一个构造函数是直接传入 Callable 对象,然后为 Callable 成员变量赋值,在异步执行任务时再调用 `Callable.call()` 方法执行异步任务逻辑。此时给任务状态 state 赋值为 NEW,表示任务新建状态。 ```java public FutureTask(Runnable runnable, V result) { this.callable = Executors.callable(runnable, result); this.state = NEW; // ensure visibility of callable } ``` 第二个构造函数是传入一个 Runnable 对象和一个指定的 `result`,然后通过 `Executors.callable()` 通过适配器 RunnableAdapter 将 Runnable 对象转换为 Callable 对象,然后再分别给 Callable 和 state 赋值。此时任务状态 state 的初始值依然为 NEW。 #### 核心方法 ##### run() FutureTask 间接实现了 Runnable 接口,覆写了 Runnable 接口的 `run()` 方法,该方法主要用于执行异步任务逻辑。下面我们来着重分析一下 `run()` 方法。 ```java public void run() { /** * 【1】为了防止多线程并发执行异步任务,这里需要判断线程是否满足执行异步任务的条件,主要有以下三种情况: * A. 若任务状态 state 为 NEW 且 runner 为 null,说明还没有线程执行过异步任务,满足执行异步任务的条件, * 此时同时调用 CAS 方法为成员变量 runner 设置为当前线程的值。 * B. 若任务状态 state 为 NEW 且 runner 不为 null,说明有线程正在执行异步任务,不满足执行异步任务的条件, * 直接返回。 * C. 若任务状态 state 不为 NEW,此时不管 runner 是否为 null,说明已经有线程执行过异步任务,没有必要再重新执行一次, * 不满足执行异步任务的条件,直接返回。 */ if (state != NEW || !UNSAFE.compareAndSwapObject(this, runnerOffset, null, Thread.currentThread())) return; try { Callable<V> c = callable; // 之前构造函数传进来的 callable 实现类对象,其 call() 方法封装了异步任务执行的逻辑 if (c != null && state == NEW) { // 若异步任务的状态还是新建状态的话,那么就调用异步任务 V result; // 异步任务执行的结果 boolean ran; // 异步任务执行成功标志 try { result = c.call(); // 【2】执行异步任务逻辑,并把执行结果赋值给 result ran = true; } catch (Throwable ex) { result = null; ran = false; // 异步任务执行过程中抛出异常,设置 ran 为 false setException(ex); // 【3】设置异常,同时更新 state 状态 } if (ran) // 【3】若异步任务执行成功,设置异步任务执行结果,同时更新 state 状态 set(result); } } finally { // 异步任务执行过程中 runner 一直是非空的,防止并发调用 run() 方法 // 在异步任务执行完成后,不管是正常结束还是异常结束,设置 runner 为 null runner = null; int s = state; // 线程执行异步任务后的任务状态 if (s >= INTERRUPTING) // 【4】如果执行了 cancel(true) 方法,调用 handlePossibleCancellationInterrupt() 方法处理中断 handlePossibleCancellationInterrupt(s); } } ``` `run()` 方法主要分为以下四步来执行: - **判断线程是否满足执行异步任务的条件**:为了防止多线程并发执行异步任务,需要判断线程是否满足执行异步任务的条件。 - **如果满足条件的话**,**执行异步任务**:因为异步任务的逻辑封装在 `Callable.call()` 方法中,此时直接调用 `Callable.call()` 方法执行异步任务,然后返回执行结果。 - **根据异步任务的执行情况做不同处理**: - 若异步任务执行**正常结束**,此时调用 `set(result)` 来设置任务执行结果。 - 若异步任务执行**抛出异常**,此时调用 `setException(ex)` 来设置异常。 - **异步任务执行完成后的善后处理工作**:不管异步任务执行成功还是失败,若其他线程调用了 `cancel(true)` 方法,此时需要调用 `handlePossibleCancellationInterrupt()` 方法处理中断。 > `set(result)` 的具体执行过程是怎么样的? > > ~~~java > protected void set(V v) { > /** > * 【1】调用 UNSAFE 的 CAS 方法判断任务当前状态是否为 NEW,如果为 NEW,则设置任务状态为 COMPLETING > * 【思考】此时任务不能被多线程并发执行,什么情况下会导致任务状态不为 NEW ? > * 只有在调用了 cancel 方法的时候,任务状态不为 NEW,此时什么都不需要做。 > */ > if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) { > outcome = v; // 【2】将任务执行结果赋值给成员变量 outcome > UNSAFE.putOrderedInt(this, stateOffset, NORMAL); // 【3】将任务状态设置为 NORMAL,表示任务正常结束 > finishCompletion(); // 【4】调用任务执行完成方法,此时会唤醒阻塞的线程,调用 done() 方法和清空等待线程链表等 > } > } > ~~~ > > 当异步任务正常执行结束,且没有被 cancel 的情况下,将任务的状态设置为 COMPLETING,然后会将任务执行结果保存到成员变量 outcome 中,将任务的状态设置为 NORMAL,最后调用 `finishCompletion()` 方法来 **唤醒阻塞的线程**(这里阻塞的线程是指我们调用 `Future.get()` 方法时若异步任务还未执行完,此时该线程会阻塞)。此时任务的状态变化是 NEW -> COMPLETING -> NORMAL。 > > 下面我们来分析一下 `finishCompletion()` 方法的具体执行过程。 > > ~~~java > private void finishCompletion() { > /** > * 取出等待线程链表头节点,判断头节点是否为 null: > * 1. 若线程链表头节点不为空,此时以“后进先出”的顺序(栈)移除等待的线程节点 > * 2. 若线程链表头节点为空,说明还没有线程调用 Future.get() 方法来获取任务执行结果,所以不用移除 > */ > for (WaitNode q; (q = waiters) != null;) { > // 调用 UNSAFE 的 CAS 方法将成员变量 waiters 设置为空 > if (UNSAFE.compareAndSwapObject(this, waitersOffset, q, null)) { > for (;;) { > Thread t = q.thread; // 取出 WaitNode 节点的线程 > if (t != null) { // 若取出的线程不为 null,则将该 WaitNode 节点线程置空,且唤醒正在阻塞的该线程 > q.thread = null; > LockSupport.unpark(t); // 唤醒正在阻塞的该线程 > } > WaitNode next = q.next; > if (next == null) // 若没有下一个 WaitNode 线程节点,说明已经将所有等待的线程唤醒,此时跳出 for 循环 > break; > q.next = null; // 将已经移除的线程 WaitNode 节点的 next 指针置空,此时好被垃圾回收 > q = next; // 再把下一个 WaitNode 线程节点置为当前线程 WaitNode 头节点 > } > break; > } > } > > done(); // 不管任务正常执行还是抛出异常,都会调用 done() 方法 > > callable = null; // 因为异步任务已经执行完成,且结果已经保存到 outcome 中,因此此时可以将 callable 对象置空了 > } > ~~~ > > `finishCompletion()` 方法的作用就是不管异步任务正常执行还是异常结束,此时都要 **唤醒且移除线程等待链表的等待线程节点**,这个链表实现的是一个 Treiber 栈,因此唤醒(移除)的顺序是 **后进先出**,即 **后面先来的线程会先被唤醒**(**移除**)。 > `setException(ex)` 的具体执行过程是怎么样的? > > ~~~java > protected void setException(Throwable t) { > /** > * 【1】调用 UNSAFE 的 CAS 方法判断任务当前状态是否为 NEW,如果为 NEW,则设置任务状态为 COMPLETING > * 【思考】此时任务不能被多线程并发执行,什么情况下会导致任务状态不为 NEW ? > * 只有在调用了 cancel 方法的时候,任务状态不为 NEW,此时什么都不需要做。 > */ > if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) { > outcome = t; // 【2】将任务执行结果赋值给成员变量 outcome > UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // 【3】将任务状态设置为 EXCEPTIONAL,表示任务正常结束 > finishCompletion(); // 【4】调用任务执行完成方法,此时会唤醒阻塞的线程,调用 done() 方法和清空等待线程链表等 > } > } > ~~~ > > `setException(Throwable t)` 的代码逻辑跟前面 `set(V v)` 几乎一样,不同的是任务执行过程中抛出异常,此时是将异常保存到 FutureTask 的成员变量 `outcom` 中,最后同样会调用 `finishCompletion()` 方法来 **唤醒阻塞的线程**。此时任务状态变化为 NEW -> COMPLETING -> EXCEPTIONAL。 > `handlePossibleCancellationInterrupt(s)` 的具体执行过程是怎么样的? > > ~~~java > private void handlePossibleCancellationInterrupt(int s) { > // 如果当前正在中断过程中,自旋等待一下,让出 CPU 执行机会,让其它线程执行,等中断完成 > if (s == INTERRUPTING) > while (state == INTERRUPTING) > Thread.yield(); // wait out pending interrupt > } > ~~~ > > 在 `run()` 方法的 `finally` 代码块中,若任务状态 `state >= INTERRUPTING`,说明有其它线程执行了 `cancel(true)` 方法,此时需要**让出 CPU 执行的时间片给其它线程执行**。 ##### get() 前面我们提到当一个线程在其 `run()` 方法中执行异步任务后,此时我们可以调用 `FutureTask.get()` 方法来获取异步任务执行的结果。 ~~~java public V get() throws InterruptedException, ExecutionException { int s = state; if (s <= COMPLETING) // 【1】若任务状态 <= COMPLETING,说明任务正在执行过程中,此时可能正常结束,也可能遇到异常 s = awaitDone(false, 0L); return report(s); // 【2】最后根据任务状态来返回任务执行结果,此时有三种情况:A. 任务正常执行;B. 任务执行异常;C. 任务被取消 } ~~~ 可以看到,如果任务状态 `state <= COMPLETING`,说明异步任务**正在执行过程中**,此时会调用 `awaitDone()` 方法阻塞线程等待。当任务执行完成后,此时再调用 `report()` 方法来报告任务结果,此时有三种情况,分别为任务正常执行、任务执行异常或任务被取消。 > `awaitDone()` 的具体执行过程是怎么样的? > > ~~~java > private int awaitDone(boolean timed, long nanos) > throws InterruptedException { > final long deadline = timed ? System.nanoTime() + nanos : 0L; // 计算超时结束时间 > WaitNode q = null; // 线程链表头节点 > boolean queued = false; // 是否入队 > for (;;) { > // 如果当前获取任务执行结果的线程被中断,此时移除该线程 WaitNode 链表节点,并抛出 InterruptedException > if (Thread.interrupted()) { > removeWaiter(q); > throw new InterruptedException(); > } > > int s = state; > // 【5】如果任务状态 > COMPLETING,返回任务执行结果,此时任务可能正常结束(NORMAL)、抛出异常(EXCEPTIONAL)、 > // 或被取消(CANCELLED、INTERRUPTING 或 INTERRUPTED 状态中的一种) > if (s > COMPLETING) { > // 【思考】此时将当前 WaitNode 节点的线程置空,其中在任务结束时也会调用 finishCompletion 将 WaitNode 节点的 thread 置空, > // 这里为什么又要再调用一次 q.thread == null 呢? > // 【答】因为若很多线程来获取任务执行结果,在任务执行完的那一刻,此时获取任务的线程要么已经在线程等待链表中,要么此时还是一个孤立的 WaitNode 节点。 > // 在线程等待链表中的所有 WaitNode 节点将由 finishCompletion来移除(同时唤醒)所有等待的 WaitNode 节点,以便垃圾回收。而孤立的线程 WaitNode > // 节点此时还未阻塞,因此不需要被唤醒,只需要将其属性置为 null,因其没有被谁引用,可以被垃圾回收。 > if (q != null) > q.thread = null; > return s; // 返回任务执行结果 > } > // 【4】若任务状态为 COMPLETING,说明任务正在执行过程中,此时获取任务的线程需要让出 CPU 执行时间片。 > else if (s == COMPLETING) > Thread.yield(); > // 【1】若当前线程还没有进入线程等待链表的 WaitNode 节点,此时新建一个 WaitNode 节点,并把当前线程赋值给 WaitNode 节点的 thread 属性 > else if (q == null) > q = new WaitNode(); > // 【2】若当前线程等待节点还未加入线程等待队列,则加入到该线程等待队列的头部 > else if (!queued) > queued = UNSAFE.compareAndSwapObject(this, waitersOffset, > q.next = waiters, q); > // 若有超时设置,那么执行处理超时获取任务结果的逻辑 > else if (timed) { > nanos = deadline - System.nanoTime(); > if (nanos <= 0L) { > removeWaiter(q); > return state; > } > LockSupport.parkNanos(this, nanos); > } > // 【3】若没有超时设置,则直接阻塞当前线程 > else > LockSupport.park(this); > } > } > ~~~ > > `awaitDone()` 方法主要做的事情总结如下: > > - 判断获取结果的线程是否**被其它线程中断**,如果是的话,则移除该线程 WaitNode 节点,并抛出 InterruptedException。 > - 判断任务状态 state > COMPLETING,如果是的话,直接返回任务执行结果。 > - 判断任务状态是否为 COMPLETING,如果是的话,则获取任务执行结果的线程让出 CPU 执行时间片。 > - 判断当前线程是否**进入线程等待链表的 WaitNode 节点**,如果没有的话,则新建一个 WaitNode 节点,并把当前线程赋值给 WaitNode 节点的 Thread 属性。 > - 判断当前节点是否**加入线程等待队列**,如果没有的话,则加入到该线程等待队列的头部。 > - 如果有**超时设置**,那么执行处理超时获取任务结果的逻辑。 > - 当前面的条件都不满足时,则阻塞当前线程。 > > 分析到这里我们发现执行异步任务只能有一个线程来执行,而获取异步任务结果可以有多个线程来获取。当异步任务还未执行完时,获取异步任务结果的线程会加入线程等待链表中,然后调用 `LockSupport.park(this)` 方法阻塞当前线程,直到异步任务执行完成,此时会调用 `finishCompletion()` 方法来唤醒并移除线程等待链表的每个 WaitNode 节点,这里的唤醒(移除)WaitNode 节点的线程是从链表头部开始的。 > `report()` 的具体执行过程是怎么样的? > > 在 `get()` 方法中,当异步任务执行结束后(不管是正常结束、异常结束或者取消),获取异步任务执行结果的线程都会被唤醒,然后继续执行 `report()` 方法报告异步任务的执行情况,此时可能会返回结果,也可能会抛出异常。 > > ```java > private V report(int s) throws ExecutionException { > // 将异步任务执行结果赋值给 x,此时 outcome 要么保存着异步任务正常执行的结果, > // 要么保存着异步任务执行过程中抛出的异常 > Object x = outcome; > // 【1】若异步任务正常执行结束,直接返回异步任务执行结果 > if (s == NORMAL) > return (V)x; > // 【2】若异步任务执行过程中,其它线程执行过 cancel 方法,此时抛出 CancellationException 异常 > if (s >= CANCELLED) > throw new CancellationException(); > // 【3】若异步任务执行过程中抛出异常,则将该异常转换成 ExecutionException 后重新抛出 > throw new ExecutionException((Throwable)x); > } > ``` ##### cancel() 接下来我们再看一下取消任务执行的方法 `cancel()`。 ```java public boolean cancel(boolean mayInterruptIfRunning) { // 【1】判断当前异步任务状态: // A. 如果异步任务没有完成(状态为 NEW),根据 mayInterruptIfRunning 的值更新异步任务的状态: // a. mayInterruptIfRunning 为 true,state 更新为 INTERRUPTING // b. mayInterruptIfRunning 为 false,state 更新为 CANCELLED // B. 如果异步任务已经完成(正常结束、抛出异常或已经被取消),则直接返回 false,即不能取消已经完成的异步任务 if (!(state == NEW && UNSAFE.compareAndSwapInt(this, stateOffset, NEW, mayInterruptIfRunning ? INTERRUPTING : CANCELLED))) return false; try { // 【2】如果 mayInterruptIfRunning 为 true,则中断执行异步任务的线程 runner if (mayInterruptIfRunning) { try { Thread t = runner; if (t != null) t.interrupt(); // 中断执行异步任务的线程 runner } finally { UNSAFE.putOrderedInt(this, stateOffset, INTERRUPTED); // 最后任务状态赋值为 INTERRUPTED } } } finally { // 【3】不管 mayInterruptIfRunning 为 true 还是 false,最后都要调用 finishCompletion 方法 // 唤醒阻塞的获取异步任务结果的线程并移除线程等待链表节点 finishCompletion(); } return true; } ``` 取消任务时首先需要 **判断当前异步任务状态**: - 如果异步任务没有完成(状态为 NEW),根据 mayInterruptIfRunning 的值更新异步任务的状态: - 如果 mayInterruptIfRunning 为 true,state 更新为 INTERRUPTING,然后中断执行异步任务的线程,最后将任务状态赋值为 INTERRUPTED。此时异步任务状态的变化过程为 NEW -> INTERRUPTING -> INTERRUPTED。 - 如果 mayInterruptIfRunning 为 false,state 更新为 CANCELLED,此时不会中断执行异步任务的线程。此时异步任务状态的变化过程为 NEW -> CANCELLED。 - 如果异步任务已经完成(正常结束、抛出异常或已经被取消),则直接返回 false,即**不能取消已经完成的异步任务**。 最后不管 mayInterruptIfRunning 为 true 还是 false,最后都要调用 `finishCompletion()` 方法**唤醒阻塞的获取异步任务结果的线程并移除线程等待链表节点**。 ## 通过线程池创建线程 Java 中创建线程池主要有两类方法,一类是通过 Executors 工厂类提供的方法,另一类是通过 ThreadPoolExecutor 类进行自定义创建。这里使用 JDK 自带的 Executors 来创建线程池对象,具体过程如下: - 定义一个 Runnable 的实现类,重写 run 方法。 - 创建一个拥有固定线程数的线程池。 - 通过 ExecutorService 对象的 execute 方法传入线程对象。 ~~~java public class MyThread4 implements Runnable { @Override public void run() { System.out.println(Thread.currentThread().getName() + " thread run..."); } public static void main(String[] args) { int nThreads = 4; ExecutorService executorService = Executors.newFixedThreadPool(nThreads); for (int i = 0; i < nThreads; i++) { executorService.execute(new MyThread4()); } executorService.shutdown(); } } ~~~ 线程池的详细内容可参考 [线程池实现原理](https://notebook.ricear.com/doc/818)。 ## 参考文献 - [java--类单继承多实现,接口多继承](https://blog.csdn.net/Schaffer_W/article/details/120745831)。 - [面试官问我:创建线程有几种方式?我笑了](https://cloud.tencent.com/developer/article/1739160)。 - [Java创建线程的三种方式以及区别](https://blog.51cto.com/u_15246373/4920696)。 - [JNI-从jvm源码分析Thread.start的调用与Thread.run的回调](https://www.cnblogs.com/tera/p/13937611.html)。 - [多线程——继承Thread 类和实现Runnable 接口的区别](https://blog.csdn.net/u010926964/article/details/74962673)。 - [Java多线程-Callable和Future](https://juejin.cn/post/6844903774985650183)。 - [Future与Callable原理](https://juejin.cn/post/6980596446119034911)。 - [Java是如何实现Future模式的?万字详解!](https://juejin.cn/post/6844904199625375757) - [Java Future的实现原理](https://www.jianshu.com/p/69a6ae850736)。 - [FutureTask源码解读](https://www.cnblogs.com/micrari/p/7374513.html)。 - [FutureTask源码解析(2)——深入理解FutureTask](https://segmentfault.com/a/1190000016572591)。 - [JUC线程池: FutureTask详解](https://pdai.tech/md/java/thread/java-thread-x-juc-executor-FutureTask.html)。 - [JAVA中创建线程池的五种方法及比较](https://www.cnblogs.com/pcheng/p/13540619.html)。
ricear
2023年5月31日 17:36
©
BY-NC-ND(4.0)
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Markdown文件
分享
链接
类型
密码
更新密码