Java多线程小结
Posted 九鼎煮鸡蛋
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java多线程小结相关的知识,希望对你有一定的参考价值。
在android中,由于性能等多方面因素,多线程使用的场景较多,基于多线程的消息机制Handler、异步处理AsyncTask、回调方法等也经常会遇到,这里简要分析下Java多线程的使用和原理(针对Thread和Runnable,Callable等不在讨论范围内)
创建多线程
java端多线程的使用比较简单,JDK提供了接口Runnable和类Thread以供多线程使用,实现run方法,执行start函数启动即可,网上例子很多,这边给出最简单的使用:
//1.继承Thread类 public class MyThread extends Thread{ @Override public void run(){ ... } } MyThread mThread = new MyThread(); mThread.start(); //2.实现Runnable接口 public class MyRunnable implements Runnable { @Override public void run(){ ... } } Thread thread = new Thread(new MyRunnable()); thread.start();
众所周知,重写run方法,就是确定线程的执行方法;而调用start函数,就是启动多线程执行。如果只调用run方法,和调用正常函数并无区别,还是在当前线程执行。
之所以在实现Runnable接口之后还需要定义Thread对象来调用start函数,就是因为Runnable接口没有start函数 (接口的方法是抽象方法,不能有具体实现,必须在子类覆盖实现,而自己实现start函数又不怎么现实),所以只能调用其子类Thread提供的start完成多线程的创建执行。
这里给出Thread和Runnable的部分源码:
//Runnable接口非常简单,只有一个虚方法run public interface Runnable { public abstract void run(); } //Thread类实现了Runnable,并提供了start方法(下面具体分析) public class Thread implements Runnable { public synchronized void start() { ... } private Runnable target; @Override public void run() { //如果Thread的run未被重写,且Runnable对象不为空,则调用Runnable的run //然而Runnable是接口,不能实例化,run方法也不能实现 //这种情况其实就是定义了类A实现了Runnable接口,并用类A的对象作为参数创建了Thread对象 if (target != null) { target.run(); } } }
除了通过定义子类的方式实现多线程,当然也可以通过使用匿名内部类,同样是依据类Thread或接口Runnable
public static void main(String[] args) { //3.匿名内部类继承thread new Thread() { public void run() { ... } }.start(); //4.匿名内部类实现runnable,同样依赖于Thread的start创建执行线程 new Thread(new Runnable() { @Override public void run() { ... } }).start(); }
从上面的使用实例可以看出,多线程的启动执行工作就在这个start函数中,下面来具体看看
start函数简介
首先看下start函数在Thread.java中的定义
public class Thread implements Runnable { public synchronized void start() { //防止一些由VM启动的线程被人为调用启动(主线程或系统组线程) if (threadStatus != 0) throw new IllegalThreadStateException(); //group主要是对线程队列的操作(记录线程状态,启动、阻塞等的计数等),对线程自身的运行无关 group.add(this); boolean started = false; try { //线程启动函数,native函数,具体实现在C++端 start0(); started = true; } finally { try { if (!started) { group.threadStartFailed(this); } } catch (Throwable ignore) { } } } private native void start0(); }
经过一些异常处理和状态记录后,启动操作交给底层的C++去实现,启动函数就是这个start0。
按照openjdk的设计,java与c++相对应的类一般选择相同名称。查找后发现在/jdk/src/java.base/share/native/libjava文件夹下有一个同名文件Thread.c。
扫视一眼,Thread.c中确实有start0,但却没有遵循传统的JNI调用函数命名规则(Java_包层级目录_函数名),而是使用了对函数进行注册的机制——RegisterNatives
RegisterNatives的作用就是向VM注册 java方法<—>C++函数 的映射关系,以便在java端调用native函数时可以快速地定位其对应的C++方法,而且便于修改:改变映射关系数组,再次调用RegisterNatives可覆盖之前的映射关系,而传统的JNI命名规则则需要修改C++方法
下面来看下Thread类是如何使用这个注册机制的:
在java端调用native函数之前,需要主动调用RegisterNatives进行native函数的注册,在Thread.java中,申明并调用了如下native函数
public class Thread implements Runnable { //系统注释:确保这个函数是该接口初始化时第一个调用的 //这个函数是用于JNI关联C++方法和native函数的,Thread中的线程操作函数并没有使用JNI中传统的名称对应规则,所以需要用这个函数保证native函数有定义可用 private static native void registerNatives(); //放在static块中,在初始化Thread类时执行一次注册即可,与对象无关 static { registerNatives(); } } //对应的C++方法,使用了传统的JNI调用函数命名规则,就在之前提到的Thread.c文件中 JNIEXPORT void JNICALL Java_java_lang_Thread_registerNatives(JNIEnv *env, jclass cls) { //向VM注册native函数的对应关系,此函数具体源码没有找到,这个methods就是native函数的映射表,下面会讲到,cls为类型,对应Java中的Thread,这样就能精确定位Java中的函数 (*env)->RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods)); }
native函数与C++方法的对应关系结构体及Thread所用到的函数映射表
//描述一个native函数对象和其对应的C++方法 typedef struct { char *name; //native函数名 char *signature; //native函数签名(参数与返回值类型) void *fnPtr; //对应的函数指针引用(C++中的具体实现函数) } JNINativeMethod; //函数映射表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}, {"setNativeName", "(" STR ")V", (void *)&JVM_SetNativeThreadName}, };
经过RegisterNatives的注册,Java端的native函数start0与C++端的JVM_StartThread形成对应关系,线程启动工作也就落在了JVM_StartThread函数中
//宏JVM_ENTRY--JVM_END,用来对函数进行定义,这里就是定义函数JVM_StartThread JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread)) JVMWrapper("JVM_StartThread"); JavaThread *native_thread = NULL; bool throw_illegal_thread_state = false; { //在java线程创建成功(加入到线程队列)之前防止C++本地线程和相关平台数据被释放(平台数据用于创建线程时选择对应平台方法,c++本地线程就是之后的操作线程) MutexLocker mu(Threads_lock); //防止对一个已存在的线程进行再次创建 if (java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread)) != NULL) { throw_illegal_thread_state = true; } else { jlong size = java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread)); size_t sz = size > 0 ? (size_t) size : 0; //创建本地线程(C++线程,根据不同平台选择不同的处理),设置线程处理函数为thread_entry(下面会讲到),这里的JavaThread就是主要的线程处理类 //其属性包括java层线程对象、jni指针、java层引用计数等以及一系列编译优化内存控制项,控制本地线程的生命周期内活动、与Java端关联等一系列操作(属性、功能很多) native_thread = new JavaThread(&thread_entry, sz); //经过上面的创建JavaThread对象之后,对象native_thread中应该包含有平台相关信息 if (native_thread->osthread() != NULL) { //将本地线程加入线程链表、设置优先级,并与java线程对象相关联 //作为参数的jthread其实就是java层调用start0的Thread类对象 //这里通过句柄将jthread和C++线程关联,通过JNI可直接操作C++线程 native_thread->prepare(jthread); } } } if (throw_illegal_thread_state) { THROW(vmSymbols::java_lang_IllegalThreadStateException()); } assert(native_thread != NULL, "Starting null thread?"); if (native_thread->osthread() == NULL) { delete native_thread; if (JvmtiExport::should_post_resource_exhausted()) { JvmtiExport::post_resource_exhausted( JVMTI_RESOURCE_EXHAUSTED_OOM_ERROR | JVMTI_RESOURCE_EXHAUSTED_THREADS, os::native_thread_creation_failed_msg()); } THROW_MSG(vmSymbols::java_lang_OutOfMemoryError(), os::native_thread_creation_failed_msg()); } //本地线程在创建JavaThread对象时已经创建并初始化,在prepare时已与Java对象关联 //线程在初始化状态默认被阻塞,在这里主要功能就是唤醒本地线程使其开始运行 Thread::start(native_thread); JVM_END
从上面的代码中可以看到,除了一些异常处理外,使用到的重要函数有三个,分别是JavaThread、prepare和start。他们的基本功能已经作了相关注释,下面来具体看下
创建线程之JavaThread
JavaThread这个类相当的大,功能相当的多,呵呵!在这里只简述下主要流程
JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) : Thread() { //初始化 initialize(); //JNI附加状态,刚开始为未附加(此时线程尚未创建,当然未关联) _jni_attach_state = _not_attaching_via_jni; //设置属性(线程函数)_entry_point为&thread_entry (参数entry_point对应的值为&thread_entry),后面会用到 set_entry_point(entry_point); //设置线程类型(便宜线程或处理线程) os::ThreadType thr_type = os::java_thread; thr_type = entry_point == &compiler_thread_entry ? os::compiler_thread : os::java_thread; //属性值设置完了,那毫无疑问最后这个函数就是最重要的线程创建函数了 //注意到函数前的作用域os没,他就是操作系统接口类,提供基于平台的代码,也就是说,create_thread会根据平台不同而不同,这里主要介绍下Linux平台下的相关代码 os::create_thread(this, thr_type, stack_sz); }
//Linux平台下对应的create_thread实现 bool os::create_thread(Thread* thread, ThreadType thr_type, size_t req_stack_size) { assert(thread->osthread() == NULL, "caller responsible"); OSThread* osthread = new OSThread(NULL, NULL); if (osthread == NULL) { return false; } //设置线程类型 osthread->set_thread_type(thr_type); //设置状态(内存分配、初始化等状态) osthread->set_state(ALLOCATED); //设置平台相关数据 thread->set_osthread(osthread); pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); //堆栈大小设置什么的就略过了 size_t stack_size = os::Posix::get_initial_stack_size(thr_type, req_stack_size); stack_size = align_size_up(stack_size + os::Linux::default_guard_size(thr_type), vm_page_size()); pthread_attr_setstacksize(&attr, stack_size); pthread_attr_setguardsize(&attr, os::Linux::default_guard_size(thr_type)); ThreadState state; { pthread_t tid; //看到这句是不是很熟悉了,创建线程,处理函数为thread_native_entry //相应的Windows下为_beginthreadex;Solaris下为thr_create int ret = pthread_create(&tid, &attr, (void* (*)(void*)) thread_native_entry, thread); char buf[64]; if (ret == 0) { log_info(os, thread)("Thread started (pthread id: " UINTX_FORMAT ", attributes: %s). ", (uintx) tid, os::Posix::describe_pthread_attr(buf, sizeof(buf), &attr)); } else { log_warning(os, thread)("Failed to start thread - pthread_create failed (%s) for attributes: %s.", os::errno_name(ret), os::Posix::describe_pthread_attr(buf, sizeof(buf), &attr)); } //删除临时数据、报错退出 pthread_attr_destroy(&attr); if (ret != 0) { // Need to clean up stuff we\'ve allocated so far thread->set_osthread(NULL); delete osthread; return false; } //向平台数据记录线程号 osthread->set_pthread_id(tid); //初始化成功或者中止 { Monitor* sync_with_child = osthread->startThread_lock(); MutexLockerEx ml(sync_with_child, Mutex::_no_safepoint_check_flag); while ((state = osthread->get_state()) == ALLOCATED) { sync_with_child->wait(Mutex::_no_safepoint_check_flag); } } } //达到上限,退出 if (state == ZOMBIE) { thread->set_osthread(NULL); delete osthread; return false; } //线程初始化成功,返回
//这里有句系统注释: The thread is returned suspended (in state INITIALIZED),也就是说,创建并初始化成功后,线程默认被阻塞,需要唤醒才能运行
assert(state == INITIALIZED, "race condition"); return true; }
经过函数create_thread处理之后,本地线程就已经创建成功并初始化,处理函数为thread_native_entry。如果正确返回,那此时线程就处于已初始化状态(此时线程尚未加入到进程的线程链表中,且被阻塞),如果返回错误,那说明创建失败。关于线程的状态,可以参考这篇文章。接下来的就是要看看thread_native_entry在哪定义,是否和我们想的一样,最终执行的是Java端所定义的函数run?
源码中安全检测类代码太多,这边就只列出主要函数
static unsigned __stdcall thread_native_entry(Thread* thread) { __try { thread->run(); } __except(topLevelExceptionFilter((_EXCEPTION_POINTERS*)_exception_info())) { } return (unsigned)os::win32::exit_process_or_thread(os::win32::EPT_THREAD, res); } void JavaThread::run() { thread_main_inner(); } void JavaThread::thread_main_inner() { if (!this->has_pending_exception() && !java_lang_Thread::is_stillborn(this->threadObj())) { { ResourceMark rm(this); this->set_native_thread_name(this->get_thread_name()); } HandleMark hm(this); this->entry_point()(this, this); } } ThreadFunction entry_point() const { return _entry_point; }
进过上面的流程过滤,最后定位到了函数 _entry_point,那这个是什么呢?往回看一点点,对,就是在创建JavaThread的函数中,有一个set_entry_point(entry_point),功能是将_entry_point的值设置为&thread_entry,也就是这边所用的这个_entry_point,而其值就是&thread_entry,这个又是什么呢?来看看定义
static void thread_entry(JavaThread* thread, TRAPS) { HandleMark hm(THREAD); Handle obj(THREAD, thread->threadObj()); JavaValue result(T_VOID); //JavaCalls: openjdk中用于C++端调用Java端方法的功能类,在这里不再深入 //显然这边就是调用最终的具体执行函数了,来看看是不是run方法 //类vmSymbols是用于VM对所用的标识进行快速定位的,在他的命名空间中,定义有一系列(函数名,符号名)的对应关系 //类SystemDictionary用作类加载器的辅助类,记录本地函数与Java类名的对应关系 JavaCalls::call_virtual(&result, obj, KlassHandle(THREAD, SystemDictionary::Thread_klass()), //类型java_lang_Thread的一个句柄 vmSymbols::run_method_name(), //函数名 vmSymbols::void_method_signature(), //函数签名 THREAD); } //由此可以看出,上面的call_virtual调用的Java端函数为 void run();而函数的定位用到obj和KlassHandle //这个obj就是传入的Java层线程对象,KlassHandle对应于类型java_lang_Thread的一个句柄 //call_virtual从名字就可以看出是虚函数调用约定,后面会涉及链接时函数定位、运行时函数定位等等,有兴趣的可以查看源码 template(run_method_name, "run") template(void_method_signature,"()V") //Pre表示该类为预加载类,本地函数Thread_klass对应Java中的类java_lang_Thread do_klass(Thread_klass,java_lang_Thread,Pre)
到这里,之前的new JavaThread(&thread_entry, sz)函数的功能大致已经清楚了,简单的概括就是:创建了一个本地线程,并使其处于已初始化状态,线程处理函数为Java层线程对象的run方法
prepare与start
prepare与start的篇幅比较少,放在一起简述下
void JavaThread::prepare(jobject jni_thread, ThreadPriority prio) { //保证当前线程占有锁定资源 assert(Threads_lock->owner() == Thread::current(), "must have threads lock"); //下面几个操作将Java层线程对象和C++层本地线程相互关联起来 //系统注释中将jni_thread说成是C++线程对象,不甚理解,如果有大大懂得可以说下~~下面按自己的理解来叙述 //将jni_thread(Java层线程对象)作为本地线程的一个句柄,thread_oop则指向jni_thread,之后通过thread_oop就可调用Java层Thread类对象 Handle thread_oop(Thread::current(), JNIHandles::resolve_non_null(jni_thread)); assert(InstanceKlass::cast(thread_oop->klass())->is_linked(), "must be initialized"); //设置_threadObj(JavaThread中用于表示Java层线程对象的属性),值为上面定义的句柄,这样就实现了C++调用Java的条件 set_threadObj(thread_oop()); //向java.lang.Thread对象的接口类注册本地线程,这样就实现了Java调用C++的条件 java_lang_Thread::set_thread(thread_oop(), this); if (prio == NoPriority) { prio = java_lang_Thread::priority(thread_oop()); assert(prio != NoPriority, "A valid priority should be present"); } //设置优先级 Thread::set_priority(this, prio); prepare_ext(); //将本地线程加入到线程队列 Threads::add(this); }
用一句话来概括prepare的工作:关联Java层线程对象与C++本地线程,并将本地线程加入线程队列
到现在为止,就差一步线程就能开始工作了(还记得上面说的线程创建后默认被阻塞么),最后的工作就是将它唤醒,当然就是start的功能了
void Thread::start(Thread* thread) { //启动线程与恢复线程情况不同,这边要排除这种情况 if (!DisableStartThread) { if (thread->is_Java_thread()) { //设置线程状态为运行 java_lang_Thread::set_thread_status(((JavaThread*)thread)->threadObj(), java_lang_Thread::RUNNABLE); } os::start_thread(thread); } } void os::start_thread(Thread* thread) { MutexLockerEx ml(thread->SR_lock(), Mutex::_no_safepoint_check_flag); OSThread* osthread = thread->osthread(); //设置平台相关数据的状态为运行 osthread->set_state(RUNNABLE); //调用各平台的唤醒函数启动线程 pd_start_thread(thread); } //Windows的比较直观,分析Windows的 void os::pd_start_thread(Thread* thread) { //唤醒本地线程使之运行,对应Linxu的notify,Solaris的thr_continue DWORD ret = ResumeThread(thread->osthread()->thread_handle()); assert(ret != SYS_THREAD_ERROR, "StartThread failed"); }
以上是关于Java多线程小结的主要内容,如果未能解决你的问题,请参考以下文章