Java线程的start方法回调run方法的操作技巧

所属分类: 软件编程 / java 阅读数: 30
收藏 0 赞 0 分享

面试中可能会被问到为什么我们调用start()方法时会执行run()方法,为什么我们不能直接调用run()方法?

Java 创建线程的方法

实际上,创建线程最重要的是提供线程函数(回调函数),该函数作为新创建线程的入口函数,实现自己想要的功能。Java 提供了两种方法来创建一个线程:

继承 Thread 类

class MyThread extends Thread{ 
 public void run() { 
  System.out.println("My thread is started."); 
 } 
}

实现该继承类的 run 方法,然后就可以创建这个子类的对象,调用 start 方法即可创建一个新的线程:

MyThread myThread = new MyThread(); 
myThread.start();

实现 Runnable 接口

class MyRunnable implements Runnable{ 
 public void run() { 
  System.out.println("My runnable is invoked."); 
 } 
}

实现 Runnable 接口的类的对象可以作为一个参数传递到创建的 Thread 对象中,同样调用 Thread#start 方法就可以在一个新的线程中运行 run 方法中的代码了。

Thread myThread = new Thread( new MyRunnable()); 
myThread.start();

可以看到,不管是用哪种方法,实际上都是要实现一个 run 方法的。 该方法本质是上一个回调方法。由 start 方法新创建的线程会调用这个方法从而执行需要的代码。 从后面可以看到,run 方法并不是真正的线程函数,只是被线程函数调用的一个 Java 方法而已,和其他的 Java 方法没有什么本质的不同。

Java 线程的实现

从概念上来说,一个 Java 线程的创建根本上就对应了一个本地线程(native thread)的创建,两者是一一对应的。 问题是,本地线程执行的应该是本地代码,而 Java 线程提供的线程函数是 Java 方法,编译出的是 Java 字节码,所以可以想象的是, Java 线程其实提供了一个统一的线程函数,该线程函数通过 Java 虚拟机调用 Java 线程方法 , 这是通过 Java 本地方法调用来实现的。

以下是 Thread#start 方法的示例:

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

可以看到它实际上调用了本地方法 start0, 该方法的声明如下:

private native void start0();

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

private static native void registerNatives(); 
static{ 
 registerNatives(); 
}

本地方法 registerNatives 是定义在 Thread.c 文件中的。Thread.c 是个很小的文件,定义了各个操作系统平台都要用到的关于线程的公用数据和操作,如代码清单 1 所示。

清单1

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}, 
 {"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}, 
};

到此,可以容易的看出 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,如清单 2 所示。

清单2

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 线程创建调用关系图

这里写图片描述

start() 创建新进程
run() 没有

PS:下面看下Java线程中run和start方法的区别

Thread类中run()和start()方法的区别如下:

run()方法:在本线程内调用该Runnable对象的run()方法,可以重复多次调用;

start()方法:启动一个线程,调用该Runnable对象的run()方法,不能多次启动一个线程;

package com.ljq.test;
public class ThreadTest {
  /**
   * 观察直接调用run()和用start()启动一个线程的差别 
   * 
   * @param args
   * @throws Exception
   */
  public static void main(String[] args){
    Thread thread=new ThreadDemo();
    //第一种
    //表明: run()和其他方法的调用没任何不同,main方法按顺序执行了它,并打印出最后一句
    //thread.run();
    //第二种
    //表明: start()方法重新创建了一个线程,在main方法执行结束后,由于start()方法创建的线程没有运行结束,
    //因此主线程未能退出,直到线程thread也执行完毕.这里要注意,默认创建的线程是用户线程(非守护线程)
    //thread.start();
    //第三种
    //1、为什么没有打印出100句呢?因为我们将thread线程设置为了daemon(守护)线程,程序中只有守护线程存在的时候,是可以退出的,所以只打印了七句便退出了
    //2、当java虚拟机中有守护线程在运行的时候,java虚拟机会关闭。当所有常规线程运行完毕以后,
    //守护线程不管运行到哪里,虚拟机都会退出运行。所以你的守护线程最好不要写一些会影响程序的业务逻辑。否则无法预料程序到底会出现什么问题
    //thread.setDaemon(true);
    //thread.start();
    //第四种
    //用户线程可以被System.exit(0)强制kill掉,所以也只打印出七句
    thread.start();
    System.out.println("main thread is over");
    System.exit(1);
  }
  public static class ThreadDemo extends Thread{
    @Override
    public void run() {
      for (int i = 0; i < 100; i++) {
        System.out.println("This is a Thread test"+i);
      }
    }
  }
}

以上所述是小编给大家介绍的Java线程的start方法回调run方法的操作技巧,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对脚本之家网站的支持!

更多精彩内容其他人还在看

浅谈spring注解之@profile

这篇文章主要介绍了浅谈spring注解之@profile,@profile通过配置来改变参数,这里整理的详细的用法,有兴趣的可以了解一下
收藏 0 赞 0 分享

Java this 关键字的使用方法详解

这篇文章主要介绍了Java this 关键字的使用方法详解的相关资料,希望通过本文能帮助到大家,让大家彻底理解掌握这部分内容,需要的朋友可以参考下
收藏 0 赞 0 分享

Java super关键字的使用方法详解

这篇文章主要介绍了Java super关键字的使用方法详解的相关资料,希望通过本文能帮助到大家,让大家对super关键字彻底掌握,需要的朋友可以参考下
收藏 0 赞 0 分享

springboot整合redis进行数据操作(推荐)

springboot整合redis比较简单,并且使用redistemplate可以让我们更加方便的对数据进行操作。下面通过本文给大家分享springboot整合redis进行数据操作的相关知识,感兴趣的朋友一起看看吧
收藏 0 赞 0 分享

java实现简单解析XML文件功能示例

这篇文章主要介绍了java实现简单解析XML文件功能,结合实例形式分析了java针对xml文件的读取、遍历节点及输出等相关操作技巧,需要的朋友可以参考下
收藏 0 赞 0 分享

Spring Java-based容器配置详解

这篇文章主要介绍了Spring Java-based容器配置详解,涉及注解和@Configuration类以及@Beans的相关知识,具有一定参考价值,需要的朋友可以了解。
收藏 0 赞 0 分享

java实现MD5加密的方法小结

这篇文章主要介绍了java实现MD5加密的方法,结合具体实例形式总结分析了java实现md5加密的常用操作技巧与使用方法,需要的朋友可以参考下
收藏 0 赞 0 分享

使用maven运行Java Main的三种方法解析

这篇文章主要介绍了使用maven运行Java Main的三种方式的相关内容,具有一定参考价值,需要的朋友可以了解下。
收藏 0 赞 0 分享

javaweb设计中filter粗粒度权限控制代码示例

这篇文章主要介绍了javaweb设计中filter粗粒度权限控制代码示例,小编觉得还是挺不错的,需要的朋友可以参考。
收藏 0 赞 0 分享

java 流操作对文件的分割和合并的实例详解

这篇文章主要介绍了java 流操作对文件的分割和合并的实例详解的相关资料,希望通过本文能帮助到大家,让大家理解掌握这部分内容,需要的朋友可以参考下
收藏 0 赞 0 分享
查看更多