引言

说到Thread大家都很熟悉,我们平常写并发代码的时候都会接触到,那么我们来看看下面这段代码是如何初始化以及执行的呢?

public class ThreadDemo {public static void main(String[] args) {new Thread().start();}
}

初始化流程

代码就一行很简单,那么这行简单的代码背后做了那些事情呢?

初始化Thread这个类

首先JVM会去加载Thread的字节码,初始化这个类,这里即调用下面这段代码:

public class Thread implements Runnable {/* Make sure registerNatives is the first thing <clinit> does. */private static native void registerNatives();static {registerNatives();}
}

是个native方法,那么我们去看看内部实现是什么,具体的目录是openjdk/jdk/src/share/native/java/lang/Thread.c, 下载地址

#define ARRAY_LENGTH(a) (sizeof(a)/sizeof(a[0]))//JVM前缀开头的方法,具体实现在JVM中
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//jclass cls即为java.lang.Thread
JNIEXPORT void JNICALL
Java_java_lang_Thread_registerNatives(JNIEnv *env, jclass cls)
{(*env)->RegisterNatives(env, cls, methods, ARRAY_LENGTH(methods));
}

可以发现具体的实现都是由这些JVM开头的方法决定的,而这几个方法的具体实现都在hotspot\src\share\vm\prims\jvm.cpp文件中,而RegisterNatives我目前的理解其实类似一个方法表,从Java方法到native方法的一个映射,具体的原理后面再研究。

初始化Thread对象

其实就是一些赋值,名字、线程ID这些,这两个变量都是static,用synchronized修饰,保证线程安全性。

    public Thread() {//nextThreadNum就是变量的自增,用synchronized修饰保证可见性init(null, null, "Thread-" + nextThreadNum(), 0);}private void init(ThreadGroup g, Runnable target, String name,long stackSize) {init(g, target, name, stackSize, null);}private void init(ThreadGroup g, Runnable target, String name,long stackSize, AccessControlContext acc) {if (name == null) {throw new NullPointerException("name cannot be null");}this.name = name;Thread parent = currentThread();// 安全相关的一坨东西..../* Stash the specified stack size in case the VM cares */this.stackSize = stackSize;/* Set thread ID */tid = nextThreadID();}private static synchronized long nextThreadID() {return ++threadSeqNumber;}

创建并启动线程

    public synchronized void start() {if (threadStatus != 0)throw new IllegalThreadStateException();group.add(this);boolean started = false;try {start0();started = true;} finally {try {if (!started) {group.threadStartFailed(this);}} catch (Throwable ignore) {/* do nothing. If start0 threw a Throwable thenit will be passed up the call stack */}}}

这里start0()是个native方法,对应jvm.cpp中的JVM_StartThread,我们看到很多方法都是JVM_ENTRY开头,JVM_END结尾,类似于{}的作用,这里是将很多公共的操作封装到了JVM_ENTRY里面.

JVM_ENTRY(void, JVM_StartThread(JNIEnv* env, jobject jthread))JVMWrapper("JVM_StartThread");JavaThread *native_thread = NULL;// We cannot hold the Threads_lock when we throw an exception,// due to rank ordering issues. Example:  we might need to grab the// Heap_lock while we construct the exception.bool throw_illegal_thread_state = false;// We must release the Threads_lock before we can post a jvmti event// in Thread::start.{// 加锁MutexLocker mu(Threads_lock);// 自从JDK 5之后 java.lang.Thread#threadStatus可以用来阻止重启一个已经启动// 的线程,所以这里的JavaThread通常为空。然而对于一个和JNI关联的线程来说,在线程// 被创建和更新他的threadStatus之前会有一个小窗口,因此必须检查这种情况if (java_lang_Thread::thread(JNIHandles::resolve_non_null(jthread)) != NULL) {throw_illegal_thread_state = true;} else {// We could also check the stillborn flag to see if this thread was already stopped, but// for historical reasons we let the thread detect that itself when it starts runningjlong size =java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread));// 为C++线程结构体分配内存并创建native线程。从java取出的stack size是有符号的,因此这里// 需要进行一次转换,避免传入负数导致创建一个非常大的栈。size_t sz = size > 0 ? (size_t) size : 0;native_thread = new JavaThread(&thread_entry, sz);// At this point it may be possible that no osthread was created for the// JavaThread due to lack of memory. Check for this situation and throw// an exception if necessary. Eventually we may want to change this so// that we only grab the lock if the thread was created successfully -// then we can also do this check and throw the exception in the// JavaThread constructor.if (native_thread->osthread() != NULL) {// Note: the current thread is not being used within "prepare".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) {// No one should hold a reference to the 'native_thread'.delete native_thread;if (JvmtiExport::should_post_resource_exhausted()) {JvmtiExport::post_resource_exhausted(JVMTI_RESOURCE_EXHAUSTED_OOM_ERROR | JVMTI_RESOURCE_EXHAUSTED_THREADS,"unable to create new native thread");}THROW_MSG(vmSymbols::java_lang_OutOfMemoryError(),"unable to create new native thread");}Thread::start(native_thread);JVM_END

基本上这里就是先加锁,做些检查,然后创建JavaThread,如果创建成功的话会调用prepare(),然后是一些异常处理,没有异常的话最后会启动线程,那么下面我们先来看看JavaThread是如何被创建的。

JavaThread::JavaThread(ThreadFunction entry_point, size_t stack_sz) :Thread()
#if INCLUDE_ALL_GCS, _satb_mark_queue(&_satb_mark_queue_set),_dirty_card_queue(&_dirty_card_queue_set)
#endif // INCLUDE_ALL_GCS
{if (TraceThreadEvents) {tty->print_cr("creating thread %p", this);}initialize();//这个方法其实就是一堆变量的初始化,不是Null就是0._jni_attach_state = _not_attaching_via_jni;set_entry_point(entry_point);// Create the native thread itself.// %note runtime_23os::ThreadType thr_type = os::java_thread;// 根据传进来的entry_point判断要创建的线程的类型。thr_type = entry_point == &compiler_thread_entry ? os::compiler_thread :os::java_thread;os::create_thread(this, thr_type, stack_sz);// _osthread可能是Null,因此我们耗尽了内存(太多的活跃线程)。我们需要抛出OOM,然而不能在这做,因为调用者可能// 还持有锁,而所有的锁都必须在抛出异常之前被释放。// 代码执行到这,线程还是suspended状态,因为线程必须被创建者直接启动。
}void JavaThread::initialize() {// Initialize fields// ...set_thread_state(_thread_new); // 线程的初始状态// ...}

JavaThreadState记录了线程记录了线程正在执行的代码在哪一部分,这个信息可能会被安全点使用到(GC),最核心的有四种:

  1. _thread_new 刚开始启动,但还没执行初始化代码,更可能还在OS初始化的层面

  2. _thread_in_native 在native代码中

  3. _thread_in_vm 在vm中执行

  4. _thread_in_Java 执行在解释或者编译后的Java代码中

每个状态都会对应一个中间的转换状态,这些额外的中间状态使得安全点的代码能够更快的处理某一线程状态而不用挂起线程。

enum JavaThreadState {_thread_uninitialized     =  0, // should never happen (missing initialization)_thread_new               =  2, // just starting up, i.e., in process of being initialized_thread_new_trans         =  3, // corresponding transition state (not used, included for completness)_thread_in_native         =  4, // running in native code_thread_in_native_trans   =  5, // corresponding transition state_thread_in_vm             =  6, // running in VM_thread_in_vm_trans       =  7, // corresponding transition state_thread_in_Java           =  8, // running in Java or in stub code_thread_in_Java_trans     =  9, // corresponding transition state (not used, included for completness)_thread_blocked           = 10, // blocked in vm_thread_blocked_trans     = 11, // corresponding transition state_thread_max_state         = 12  // maximum thread state+1 - used for statistics allocation
};

我们看到 os::create_thread(this, thr_type, stack_sz);这行代码会去实际的创建线程,首先我们知道Java宣传的是一次编译,到处运行,那么究竟是怎么做到在不同的CPU、操作系统上还能够保持良好的可移植性呢?

// 平台相关的东东
#ifdef TARGET_OS_FAMILY_linux
# include "os_linux.hpp"
# include "os_posix.hpp"
#endif
#ifdef TARGET_OS_FAMILY_solaris
# include "os_solaris.hpp"
# include "os_posix.hpp"
#endif
#ifdef TARGET_OS_FAMILY_windows
# include "os_windows.hpp"
#endif
#ifdef TARGET_OS_FAMILY_aix
# include "os_aix.hpp"
# include "os_posix.hpp"
#endif
#ifdef TARGET_OS_FAMILY_bsd
# include "os_posix.hpp"
# include "os_bsd.hpp"
#endif
#ifdef TARGET_OS_ARCH_linux_x86
# include "os_linux_x86.hpp"
#endif
#ifdef TARGET_OS_ARCH_linux_sparc
# include "os_linux_sparc.hpp"
#endif
#ifdef TARGET_OS_ARCH_linux_zero
# include "os_linux_zero.hpp"
#endif
#ifdef TARGET_OS_ARCH_solaris_x86
# include "os_solaris_x86.hpp"
#endif
#ifdef TARGET_OS_ARCH_solaris_sparc
# include "os_solaris_sparc.hpp"
#endif
#ifdef TARGET_OS_ARCH_windows_x86
# include "os_windows_x86.hpp"
#endif
#ifdef TARGET_OS_ARCH_linux_arm
# include "os_linux_arm.hpp"
#endif
#ifdef TARGET_OS_ARCH_linux_ppc
# include "os_linux_ppc.hpp"
#endif
#ifdef TARGET_OS_ARCH_aix_ppc
# include "os_aix_ppc.hpp"
#endif
#ifdef TARGET_OS_ARCH_bsd_x86
# include "os_bsd_x86.hpp"
#endif
#ifdef TARGET_OS_ARCH_bsd_zero
# include "os_bsd_zero.hpp"
#endif

我们看到os.hpp中有这样一段代码,能够根据不同的操作系统选择include不同的头文件,从而将平台相关的逻辑封装到对应的库文件中,我们这里以linux为例,create_thread最终会调用os_linux.cpp中的create_thread方法。


bool os::create_thread(Thread* thread, ThreadType thr_type, size_t stack_size) {assert(thread->osthread() == NULL, "caller responsible");// Allocate the OSThread objectOSThread* osthread = new OSThread(NULL, NULL);if (osthread == NULL) {return false;}// set the correct thread stateosthread->set_thread_type(thr_type);// 初始状态为ALLOCATED,而不是INITIALIZEDosthread->set_state(ALLOCATED);thread->set_osthread(osthread);// init thread attributespthread_attr_t attr;pthread_attr_init(&attr);pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED);// stack sizeif (os::Linux::supports_variable_stack_size()) {// 如果上层未传递则计算stack_sizeif (stack_size == 0) {//如果为compiler_thread,则分配4M,否则默认会分配1Mstack_size = os::Linux::default_stack_size(thr_type);switch (thr_type) {case os::java_thread:// Java线程用ThreadStackSize,这个值可以通过-Xss指定assert (JavaThread::stack_size_at_create() > 0, "this should be set");stack_size = JavaThread::stack_size_at_create();break;case os::compiler_thread:if (CompilerThreadStackSize > 0) {stack_size = (size_t)(CompilerThreadStackSize * K);break;} // else fall through:// use VMThreadStackSize if CompilerThreadStackSize is not definedcase os::vm_thread:case os::pgc_thread:case os::cgc_thread:case os::watcher_thread:if (VMThreadStackSize > 0) stack_size = (size_t)(VMThreadStackSize * K);break;}}// 用两者较大的那个,min_stack_allowed默认为128Kstack_size = MAX2(stack_size, os::Linux::min_stack_allowed);pthread_attr_setstacksize(&attr, stack_size);} else {// let pthread_create() pick the default value.}// glibc guard pagepthread_attr_setguardsize(&attr, os::Linux::default_guard_size(thr_type));ThreadState state;{// 检查是否需要加锁bool lock = os::Linux::is_LinuxThreads() && !os::Linux::is_floating_stack();if (lock) {os::Linux::createThread_lock()->lock_without_safepoint_check();}pthread_t tid;// Linux用于创建线程的函数,这个线程通过执行java_start来启动,其中thread是作为java_start的参数传递进来的// 具体可见手册:http://man7.org/linux/man-pages/man3/pthread_create.3.htmlint ret = pthread_create(&tid, &attr, (void* (*)(void*)) java_start, thread);pthread_attr_destroy(&attr);if (ret != 0) {// 创建失败,将_osthread置为空,还记得在jvm.cpp的JVM_StartThread中会根据_osthread是否为空来判断// 是否创建成功if (PrintMiscellaneous && (Verbose || WizardMode)) {perror("pthread_create()");}// 清理资源,并解锁thread->set_osthread(NULL);delete osthread;if (lock) os::Linux::createThread_lock()->unlock();return false;}// 创建成功会将底层线程的ID保存在tid中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 (lock) {os::Linux::createThread_lock()->unlock();}}// 线程的数目达到极限了if (state == ZOMBIE) {thread->set_osthread(NULL);delete osthread;return false;}// The thread is returned suspended (in state INITIALIZED),// and is started higher up in the call chainassert(state == INITIALIZED, "race condition");return true;
}

下面我们来看看pthread_create会执行的回调函数java_start,这个方法是所有新创建的线程必走的流程。

static void *java_start(Thread *thread) {// 尝试随机化热栈帧高速缓存行的索引,这有助于优化拥有相同栈帧线程去互相驱逐彼此的缓存行时,线程// 可以是同一个JVM实例或者不同的JVM实例,这尤其有助于拥有超线程技术的处理器。static int counter = 0;int pid = os::current_process_id();alloca(((pid ^ counter++) & 7) * 128);ThreadLocalStorage::set_thread(thread);OSThread* osthread = thread->osthread();Monitor* sync = osthread->startThread_lock();// non floating stack LinuxThreads needs extra check, see aboveif (!_thread_safety_check(thread)) {// notify parent threadMutexLockerEx ml(sync, Mutex::_no_safepoint_check_flag);osthread->set_state(ZOMBIE);sync->notify_all();return NULL;}// thread_id is kernel thread id (similar to Solaris LWP id)osthread->set_thread_id(os::Linux::gettid());if (UseNUMA) {int lgrp_id = os::numa_get_group_id();if (lgrp_id != -1) {thread->set_lgrp_id(lgrp_id);}}// initialize signal mask for this threados::Linux::hotspot_sigmask(thread);// initialize floating point control registeros::Linux::init_thread_fpu_state();// handshaking with parent thread{MutexLockerEx ml(sync, Mutex::_no_safepoint_check_flag);// 设置为已经初始化完成,并notify父线程osthread->set_state(INITIALIZED);sync->notify_all();// wait until os::start_thread()while (osthread->get_state() == INITIALIZED) {sync->wait(Mutex::_no_safepoint_check_flag);}}//这里上层传递过来的是JavaThread,因此会调用JavaThread#run()方法  thread->run();return 0;
}void JavaThread::run() {// 初始化本地线程分配缓存(TLAB)相关的属性this->initialize_tlab();// used to test validitity of stack trace backsthis->record_base_of_stack_pointer();// Record real stack base and size.this->record_stack_base_and_size();// Initialize thread local storage; set before calling MutexLockerthis->initialize_thread_local_storage();this->create_stack_guard_pages();this->cache_global_variables();// 将线程的状态更改为_thread_in_vm,线程已经可以被VM中的安全点相关的代码处理了,也就是说必须// JVM如果线程在执行native里面的代码,是搞不了安全点的,待确认ThreadStateTransition::transition_and_fence(this, _thread_new, _thread_in_vm);assert(JavaThread::current() == this, "sanity check");assert(!Thread::current()->owns_locks(), "sanity check");DTRACE_THREAD_PROBE(start, this);// This operation might block. We call that after all safepoint checks for a new thread has// been completed.this->set_active_handles(JNIHandleBlock::allocate_block());if (JvmtiExport::should_post_thread_life()) {JvmtiExport::post_thread_start(this);}EventThreadStart event;if (event.should_commit()) {event.set_javalangthread(java_lang_Thread::thread_id(this->threadObj()));event.commit();}// We call another function to do the rest so we are sure that the stack addresses used// from there will be lower than the stack base just computedthread_main_inner();// Note, thread is no longer valid at this point!
}void JavaThread::thread_main_inner() {assert(JavaThread::current() == this, "sanity check");assert(this->threadObj() != NULL, "just checking");// Execute thread entry point unless this thread has a pending exception// or has been stopped before starting.// Note: Due to JVM_StopThread we can have pending exceptions already!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);// 这个entry_point就是JVM_StartThread中传递过来的那个,也就是thread_entrythis->entry_point()(this, this);}DTRACE_THREAD_PROBE(stop, this);this->exit(false);delete this;
}

我们最后再看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()),vmSymbols::run_method_name(),vmSymbols::void_method_signature(),THREAD);
}

vmSymbols,这个是JVM对于那些需要特殊处理的类、方法等的声明,我的理解就是一个方法表,根据下面这行代码可以看出来,其实调用的就是run()方法.

  /* common method and field names */                                                            template(run_method_name,                           "run")

然后我们回到JVM_StartThread方法中,这里会接着调用prepare()方法,设置线程优先级(将Java中的优先级映射到os中),然后添加到线程队列中去.最后会调用Thread::start(native_thread);
启动线程。

void Thread::start(Thread* thread) {trace("start", thread);// start和resume不一样,start被synchronized修饰if (!DisableStartThread) {if (thread->is_Java_thread()) {// 在启动线程之前初始化线程的状态为RUNNABLE,为啥不能在之后设置呢?因为启动之后可能是//  等待或者睡眠等其他状态,具体是什么我们不知道java_lang_Thread::set_thread_status(((JavaThread*)thread)->threadObj(),java_lang_Thread::RUNNABLE);}os::start_thread(thread);}
}

总结

  1. 一个Java线程对应一个JavaThread->OSThread -> Native Thread

  2. 在调用java.lang.Thread#start()方法之前不会启动线程,仅仅调用run()方法只是会在当前线程运行而已

  3. //todo

Java线程那点事儿相关推荐

  1. 一些关于java线程编程的事儿

    最近要写一个多线程服务器,涉及到多个线程响应客户端的请求,甚至多个线程要访问数据库(MySQL)的问题:所以仔细的又重新的了解与线程编程相关的一些概念,下面是在查询资料时总结的一些细节,于此贴出,希望 ...

  2. java线程池队列场景,Java面试题汇总

    01 并发宝典:面试专题 面试专题分为四个部分,分别如下 Synchronized 相关问题 可重入锁 ReentrantLock 及其他显式锁相关问题 Java 线程池相关问题 Java 内存模型相 ...

  3. 四种Java线程池用法解析

    四种Java线程池用法解析 本文为大家分析四种Java线程池用法,供大家参考,具体内容如下 http://www.jb51.net/article/81843.htm 1.new Thread的弊端 ...

  4. 【java线程】锁机制:synchronized、Lock、Condition

    [Java线程]锁机制:synchronized.Lock.Condition 原创 2013年08月14日 17:15:55 标签:Java /多线程 74967 http://www.infoq. ...

  5. java线程安全总结 - 1 (转载)

    原文地址:http://www.jameswxx.com/java/java%E7%BA%BF%E7%A8%8B%E5%AE%89%E5%85%A8%E6%80%BB%E7%BB%93/ 最近想将ja ...

  6. java线程钥匙_Java多线程并发编程/锁的理解

    一.前言 最近项目遇到多线程并发的情景(并发抢单&恢复库存并行),代码在正常情况下运行没有什么问题,在高并发压测下会出现:库存超发/总库存与sku库存对不上等各种问题. 在运用了 限流/加锁等 ...

  7. 面试必问---Java线程池8大拒绝策略

    前言 谈到java的线程池最熟悉的莫过于ExecutorService接口了,jdk1.5新增的java.util.concurrent包下的这个api,大大的简化了多线程代码的开发.而不论你用Fix ...

  8. Java线程怎样映射到操作系统线程

    先说多线程模型,参考经典教材<Operating System Concepts , Silberschatz ,9th edition> 中文版是<操作系统概念,第9版> h ...

  9. java线程的优先级是数字越大优先级越高_《深入理解Java虚拟机》5分钟速成:12章(Java内存模型与线程)...

    第12章 Java内存模型与线程 前言: 1.物理机如何处理并发问题? 2.什么是Java内存模型? 3.原子性.可见性.有序性的具体含义和应用实现? 4.volatile 关键字特性? 5.基于vo ...

最新文章

  1. .NET分布式缓存Memcached测试体验
  2. Py之mpld3:mpld3的简介、安装、使用方法之详细攻略
  3. Linux 上 GDM 登录界面如何适应高分屏
  4. eplise怎么连接数据库_Eclipse连接MySQL数据库(傻瓜篇)
  5. Component template should contain exactly one root element---基于Vue的uniapp手机端_前端UI_uview工作笔记002
  6. 标准模板库 STL—— set 列传
  7. kafka练习:创建topic以及生产与消费
  8. DIV+CSS样式表命名的规则方法
  9. 大厂的 404 页面都长啥样?看到最后一个,我笑了。。。
  10. 基于SSH的房屋租赁管理系统
  11. 把显存用在刀刃上!17 种 pytorch 节约显存技巧
  12. 如何设置和解除PDF文件保护?
  13. PVM and MPI 比较
  14. 记录踩过的坑-WPS文字
  15. win10系统删除chrome浏览器输入框的历史记录
  16. 鼠标和蓝牙耳机的融合(外观专利)
  17. 阿里云邮箱短信验证和阿里云手机短信发送
  18. 无损音乐刻录成cd有意义吗_Mac装机必备之拯救歌荒,好用的五款Mac音乐播放器推荐!...
  19. DSP CCS 12.00 芯片:TMS320F28335 建立工程 ,使LED 灯闪烁
  20. 关于SATA磁盘修复方法介绍

热门文章

  1. Windows7启动信息修改
  2. VC下sql+ado数据库v_strTemp.vt == VT_NULL判断的问题
  3. 数据库安装时的log路径
  4. 为什么莫名其妙的就被调用了
  5. 大数据之-Hadoop3.x_MapReduce_ReduceJoin案例Reducer_案例完成---大数据之hadoop3.x工作笔记0131
  6. 持续集成部署Jenkins工作笔记0010---创建工程并配置源码管理
  7. STM32工作笔记0074---UCOSIII 任务管理(中)
  8. SpringCloud工作笔记086---SpringBoot启动报错:No active profile set, falling back to default profiles
  9. Android学习笔记---20_采用ContentProvider对外共享数据, UriMatcher类使用介绍 ,ContentUris类使用介绍,使用ContentResolver操作Conte
  10. java零碎总结---java实现截图并保存到本地