关于java:Thread-Runable-Callable-还傻傻分不清

1次阅读

共计 6456 个字符,预计需要花费 17 分钟才能阅读完成。

一、Thread 与 Runnable

1、创立线程的两种办法

在 java 中你怎么创立线程?置信你很快可能想到继承 Thread 类和实现 Runnable 接口这两种形式。

没错,java 提供了这两种形式来创立新的线程。网上也有各种文章介绍这两种形式创立线程的区别,然而咱们这里要讲的是这两种形式的关联。先别离看看这两种形式的代码

1、继承 Thread 类,重写 run 办法

public class MyThread extends Thread {    
    
    @Override
    public void run() {System.out.println("我是继承 Thread 类创立的线程哟");
    }    
    
    public static void main(String[] args) {MyThread myThread = new MyThread();
        myThread.start();}
}

2、实现 Runnable 接口,实现 run 办法

public class MyRunnable implements Runnable {    
    
    @Override
    public void run() {System.out.println("我是实现 Runnable 接口创立的线程哟");
    }    
    
    public static void main(String[] args) {MyRunnable myRunnable = new MyRunnable();        
        new Thread(myRunnable).start();}
}

   通过下面的代码咱们不难发现,第一种形式中,继承了 Thread 类的子类通过重写父类的 run 办法就能够实现线程的创立。

而第二种形式中实现 Runnable 接口的类的对象能够作为一个参数传递到创立的 thread 对象中。那么 Runnable 为何方神圣?跟 Thread 类之间又有哪些鲜为人知的机密?咱们下期………咱们上面揭晓。

2、Thread 与 Runnable 的关联

   咱们先看下 Runnable 的源码:

public interface Runnable {public abstract void run();
}

What? Runnable 接口这么简略?就一个 run 办法?

是的,你没有看错,Runnable 接口就这么简略。如果没有 Thread 类,那么 Runnable 接口就是一般得不能再一般的一个接口了,咱们在代码中实现这个接口,也做不了任何事件!但因为失去 Thread 类的“青眼”,这个接口就变得不个别了!

那么 Runnable 接口失去 Thread 类的青眼,具体表现在哪呢?咱们无妨先看看 Thread 类的定义

public class Thread implements Runnable{}

   原来 Thread 是 Runnable 的一个实现类!!!以程序人的第一直觉,那 Thread 天然应该实现了 run 办法,咱们持续在源码中搜查!

@Override
public void run() {if (target != null) {target.run();
  }
}

果不其然,Thread 实现了 run 办法,并且有个判断,当 target 为 null 的时候什么也不做,否则执行 target 的 run 办法,target 又是什么呢?

private Runnable target;

target 也是一个 Runnable 对象,那这个公有的字段在哪里赋值的呢?咱们持续寻找发现是在 init 办法外面进行赋值的,并且最终在构造函数中调用了 init 办法,咱们看看构造函数的定义(Thread 重载了多个构造函数)

public Thread(Runnable target) {init(null, target, "Thread-" + nextThreadNum(), 0);
}

这里咱们能看到 Thread 的构造函数反对 Runnable 的参数,不晓得到目前大家有没有捋分明 Thread 与 Runnable 的关系,咱们再总结一下:

1、Runnable 本来平民,只是一个一般的接口。
2、Thread 实现了 Runnable 接口,并且实现了接口的 run 办法。
3、Thread 提供了重载的构造函数,接管 Runnable 类型的参数。在 Thread 重写的 run 办法中对调用了构造函数传入的 Runnable 实现类的 run 办法。

   所以不论咱们用哪种形式创立线程,都要实现或者重写 run 办法!并且这个 run 办法都是实现的 Runnable 接口中的办法。这个 run 办法就是咱们自定义的须要在线程中解决的一些逻辑!那这个 run 办法在哪里调用的呢?间接调用 run 办法能够创立新的线程么?为什么咱们在代码中都是调用的 start 办法去启动一个线程?start 办法与 run 办法有什么关联?

好奇心的驱使,我再次关上了源码中的 start 办法一探到底,发现这个办法中最次要的行为是调用了一个名为 start0 的办法。

public synchronized void start() {
  ……
  start0();
  ……
}

那 start0 又是什么呢?

private native void start0();

看到 native 关键字咱们就应该晓得,这个办法是 JVM 的办法,具体的实现须要查看 C 的代码!

3、start 办法与 run 办法的关联

鉴于本人 C 语言很烂,且多年没有碰过了,然而又想弄清楚 start 办法与 run 办法的关联,于是在网上查找了相干的材料,上面做一下整顿。

参考资料:

https://www.ibm.com/developer…

在 Thread 类的顶部,有个 native 的 registerNatives 本地办法,该办法次要的作用就是注册一些本地办法供 Thread 类应用,如 start0(),stop0() 等等,能够说,所有操作本地线程的本地办法都是由它注册的 . 这个办法放在一个 static 语句块中,这就表明,当该类被加载到 JVM 中的时候,它就会被调用,进而注册相应的本地办法。

private static native void registerNatives();    

  static {registerNatives();
  }
  
}  

   本地办法 registerNatives 是定义在 Thread.c 文件中的。Thread.c 是个很小的文件,定义了各个操作系统平台都要用到的对于线程的专用数据和操作

JNIEXPORT void JNICALL
    Java_Java_lang_Thread_registerNatives (JNIEnv *env, jclass cls){(*env)->RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods));
    }
    
    static JNINativeMethod methods[] = {
       ……
{"start0", "()V",(void *)&JVM_StartThread},
       {"stop0", "(" OBJ ")V", (void *)&JVM_StopThread},
      ……
};

  到此,能够容易的看出 Java 线程调用 start 的办法,实际上会调用到 JVM_StartThread 办法,那这个办法又是怎么的逻辑呢。实际上,咱们须要的是(或者说 Java 体现行为)该办法最终要调用 Java 线程的 run 办法,事实的确如此。在 jvm.cpp 中,有如下代码段:

JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))
      ……
     native_thread = new JavaThread(&thread_entry, sz);
     ……

   这里 JVM_ENTRY 是一个宏,用来定义 JVM_StartThread 函数,能够看到函数内创立了真正的平台相干的本地线程,其线程函数是 thread_entry,代码如下所示。

static void thread_entry(JavaThread* thread, TRAPS) {HandleMark hm(THREAD);
   Handle obj(THREAD, thread->threadObj());
   JavaValue result(T_VOID);
   JavaCalls::call_virtual(&result,obj,
   KlassHandle(THREAD,SystemDictionary::Thread_klass()),
   vmSymbolHandles::run_method_name(),
   vmSymbolHandles::void_method_signature(),THREAD);
}

  能够看到调用了 vmSymbolHandles::run_method_name 办法,这是在 vmSymbols.hpp 用宏定义的:

class vmSymbolHandles: AllStatic {…         template(run_method_name,"run")
  …
}

   至于 run_method_name 是如何申明定义的,因为波及到很繁琐的代码细节,本文不做赘述。感兴趣的读者能够自行查看 JVM 的源代码。

综上所述,Java 线程的创立调用过程如上图所示,首先 , Java 线程的 start 办法会创立一个本地线程(通过调用 JVM_StartThread),该线程的线程函数是定义在 jvm.cpp 中的 thread_entry,由其再进一步调用 run 办法。能够看到 Java 线程的 run 办法和一般办法其实没有本质区别,间接调用 run 办法不会报错,然而却是在以后线程执行,而不会创立一个新的线程。

二、FutureTask、Callable 与 Runnable

1、创立能获取后果的异步线程

下面咱们理解了创立线程创立的两种形式,然而咱们也能看到,通过 runnable 形式创立的线程无奈获取返回值,如果咱们须要异步执行某个操作,并且失去返回的后果,可能下面的两种创立线程的形式就不实用啦(也不是说不能够,比方通过共享变量等形式,然而应用起来会比拟麻烦)!在 java 中提供一种比拟不便的形式,那就是应用 FutureTask 类,咱们先看看应用形式:

public class MyFutrueTask implements Callable<String> {

    @Override
    public String call() throws Exception {System.out.println("我是 Future 模式的线程啦");

        return "Future 模式的线程完结啦";
    }

    public static void main(String[] args) throws TimeoutException, ExecutionException, InterruptedException {FutureTask<String> futureTask = new FutureTask<String>(new MyFutrueTask());
        new Thread(futureTask).start();

        String result = futureTask.get(2000, TimeUnit.MILLISECONDS);

        System.out.println(result);
    }

}

   主类实现了一个名为 Callable 的接口,并且实现了接口的 call 办法,具体的业务逻辑就在 call 办法中实现!实现 Callable 接口的类的对象能够作为一个参数传递到创立的 FutureTask 对象的构造函数中,而 FutureTask 类的对象又作为一个参数传递到 Thread 对象的构造函数中……

依据下面咱们对 Thread 和 Runnable 的理解,可能作为参数传入到 Thread 构造函数的对象,肯定是实现了 Runnable 接口的!那是不是说 FutureTask 对象就应该是一个 Runnable 的实现类呢?

2、FutureTask 实现

咱们先看看 FutureTask 类的定义

public class FutureTask<V> implements RunnableFuture<V>

FutureTask 是一个泛型类,并且实现了 RunnableFutrue 泛型接口,咱们持续跟进

public interface RunnableFuture<V> extends Runnable, Future<V> {void run();
}

RunnableFutrue 接口继承了 Runnable 接口,这也就是说 FutureTask 间接的实现了 Runnable 接口,FutureTask 也是 Runnable 的一个实现类,那也就必须要实现 run 办法,还可能将实例对象传入 Thread 对象的结构后函数!RunnableFutrue 接口还继承了另外的一个名为 Futrue 的泛型接口,咱们看看该接口的定义

public interface Future<V> {boolean cancel(boolean mayInterruptIfRunning);
   boolean isCancelled();
   boolean isDone();
   V get();
   V get(long timeout, TimeUnit unit);
}

依据这些办法的命名可能看进去,FutureTask 实现了 Future 接口后,就应该领有了勾销线程、判断线程运行状态、获取后果等性能!

咱们整体看一下 FutureTask 的类图:

   FutureTask 类中对这些接口的具体实现是怎么样的呢?咱们能够到 FutureTask 类中一探到底,先瞅瞅构造函数

public interface Future<V> {boolean cancel(boolean mayInterruptIfRunning);
   boolean isCancelled();
   boolean isDone();
   V get();
   V get(long timeout, TimeUnit unit);
}

   构造函数接管一个 Callable 类型的参数,Callable 是一个泛型类型的接口,该接口只有一个名为 call 的办法。原本这也只是一个一般的接口,但因为收到 FutrueTask 的“青眼”,这个接口变得不个别了!

public interface Callable<V> {V call() throws Exception;
}

   乍一看是不是感觉跟 Runnable 接口很像呢?然而 Callable 接口的 call 办法有返回值!Callable、Runnable、FutureTask 之间是怎么关联起来的呢?咱们下面有说了 FutureTask 是 Runnable 的一个实现类,那 FutureTask 是不是应该也实现了 run 办法呢?咱们跟进一下代码:

public void run() {
    ......
    try {
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {result = c.call();
                ran = true;
            } catch (Throwable ex) {......}
            if (ran)
                set(result);
        }
    } finally {......}
}

Run 办法的实现也比较简单,上述代码只保留了须要关注的代码。

在 run 办法中调用了构造函数传入的 Callable 实现类的 call 办法,并且用 result 的变量接管办法的返回值,最初调用 set 办法将返回后果设置到类的属性,因为 FutureTask 实现了 Future 接口,所以也就有了获取返回值以及判断线程是否运行实现、勾销的能力!

也就是说,咱们自定义 Callable 实现类的 call 办法,最终会在 FutureTask 类(也就是 Runnable)的 run 办法中执行!而 Runnable 中的 run 办法,最终又会在 Thread 类的 run 办法中执行!!!
近期热文举荐:

1.Java 15 正式公布,14 个新个性,刷新你的认知!!

2. 终于靠开源我的项目弄到 IntelliJ IDEA 激活码了,真香!

3. 我用 Java 8 写了一段逻辑,共事直呼看不懂,你试试看。。

4. 吊打 Tomcat,Undertow 性能很炸!!

5.《Java 开发手册(嵩山版)》最新公布,速速下载!

感觉不错,别忘了顺手点赞 + 转发哦!

正文完
 0