摘要:然而對于一個和關(guān)聯(lián)的線程來說在線程被創(chuàng)建和更新他的之前會有一個小窗口,因此必須檢查這種情況為線程結(jié)構(gòu)體分配內(nèi)存并創(chuàng)建線程??赡苁?,因此我們耗盡了內(nèi)存太多的活躍線程。代碼執(zhí)行到這,線程還是狀態(tài),因為線程必須被創(chuàng)建者直接啟動。
引言
說到Thread大家都很熟悉,我們平常寫并發(fā)代碼的時候都會接觸到,那么我們來看看下面這段代碼是如何初始化以及執(zhí)行的呢?
public class ThreadDemo { public static void main(String[] args) { new Thread().start(); } }初始化流程
代碼就一行很簡單,那么這行簡單的代碼背后做了那些事情呢?
初始化Thread這個類首先JVM會去加載Thread的字節(jié)碼,初始化這個類,這里即調(diào)用下面這段代碼:
public class Thread implements Runnable { /* Make sure registerNatives is the first thingdoes. */ private static native void registerNatives(); static { registerNatives(); } }
是個native方法,那么我們?nèi)タ纯磧?nèi)部實現(xiàn)是什么,具體的目錄是openjdk/jdk/src/share/native/java/lang/Thread.c, 下載地址
#define ARRAY_LENGTH(a) (sizeof(a)/sizeof(a[0])) //JVM前綴開頭的方法,具體實現(xiàn)在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)); }
可以發(fā)現(xiàn)具體的實現(xiàn)都是由這些JVM開頭的方法決定的,而這幾個方法的具體實現(xiàn)都在hotspotsrcsharevmprimsjvm.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(); // 安全相關(guān)的一坨東西.... /* 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; }創(chuàng)建并啟動線程
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 then it will be passed up the call stack */ } } }
這里start0()是個native方法,對應(yīng)jvm.cpp中的JVM_StartThread,我們看到很多方法都是JVM_ENTRY開頭,JVM_END結(jié)尾,類似于{}的作用,這里是將很多公共的操作封裝到了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可以用來阻止重啟一個已經(jīng)啟動 // 的線程,所以這里的JavaThread通常為空。然而對于一個和JNI關(guān)聯(lián)的線程來說,在線程 // 被創(chuàng)建和更新他的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 running jlong size = java_lang_Thread::stackSize(JNIHandles::resolve_non_null(jthread)); // 為C++線程結(jié)構(gòu)體分配內(nèi)存并創(chuàng)建native線程。從java取出的stack size是有符號的,因此這里 // 需要進行一次轉(zhuǎn)換,避免傳入負數(shù)導(dǎo)致創(chuàng)建一個非常大的棧。 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
基本上這里就是先加鎖,做些檢查,然后創(chuàng)建JavaThread,如果創(chuàng)建成功的話會調(diào)用prepare(),然后是一些異常處理,沒有異常的話最后會啟動線程,那么下面我們先來看看JavaThread是如何被創(chuàng)建的。
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_23 os::ThreadType thr_type = os::java_thread; // 根據(jù)傳進來的entry_point判斷要創(chuàng)建的線程的類型。 thr_type = entry_point == &compiler_thread_entry ? os::compiler_thread : os::java_thread; os::create_thread(this, thr_type, stack_sz); // _osthread可能是Null,因此我們耗盡了內(nèi)存(太多的活躍線程)。我們需要拋出OOM,然而不能在這做,因為調(diào)用者可能 // 還持有鎖,而所有的鎖都必須在拋出異常之前被釋放。 // 代碼執(zhí)行到這,線程還是suspended狀態(tài),因為線程必須被創(chuàng)建者直接啟動。 } void JavaThread::initialize() { // Initialize fields // ... set_thread_state(_thread_new); // 線程的初始狀態(tài) // ... }
JavaThreadState記錄了線程記錄了線程正在執(zhí)行的代碼在哪一部分,這個信息可能會被安全點使用到(GC),最核心的有四種:
_thread_new 剛開始啟動,但還沒執(zhí)行初始化代碼,更可能還在OS初始化的層面
_thread_in_native 在native代碼中
_thread_in_vm 在vm中執(zhí)行
_thread_in_Java 執(zhí)行在解釋或者編譯后的Java代碼中
每個狀態(tài)都會對應(yīng)一個中間的轉(zhuǎn)換狀態(tài),這些額外的中間狀態(tài)使得安全點的代碼能夠更快的處理某一線程狀態(tài)而不用掛起線程。
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);這行代碼會去實際的創(chuàng)建線程,首先我們知道Java宣傳的是一次編譯,到處運行,那么究竟是怎么做到在不同的CPU、操作系統(tǒng)上還能夠保持良好的可移植性呢?
// 平臺相關(guān)的東東 #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中有這樣一段代碼,能夠根據(jù)不同的操作系統(tǒng)選擇include不同的頭文件,從而將平臺相關(guān)的邏輯封裝到對應(yīng)的庫文件中,我們這里以linux為例,create_thread最終會調(diào)用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 object OSThread* osthread = new OSThread(NULL, NULL); if (osthread == NULL) { return false; } // set the correct thread state osthread->set_thread_type(thr_type); // 初始狀態(tài)為ALLOCATED,而不是INITIALIZED osthread->set_state(ALLOCATED); thread->set_osthread(osthread); // init thread attributes pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_DETACHED); // stack size if (os::Linux::supports_variable_stack_size()) { // 如果上層未傳遞則計算stack_size if (stack_size == 0) { //如果為compiler_thread,則分配4M,否則默認會分配1M stack_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 defined case 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默認為128K stack_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 page pthread_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用于創(chuàng)建線程的函數(shù),這個線程通過執(zhí)行java_start來啟動,其中thread是作為java_start的參數(shù)傳遞進來的 // 具體可見手冊:http://man7.org/linux/man-pages/man3/pthread_create.3.html int ret = pthread_create(&tid, &attr, (void* (*)(void*)) java_start, thread); pthread_attr_destroy(&attr); if (ret != 0) { // 創(chuàng)建失敗,將_osthread置為空,還記得在jvm.cpp的JVM_StartThread中會根據(jù)_osthread是否為空來判斷 // 是否創(chuàng)建成功 if (PrintMiscellaneous && (Verbose || WizardMode)) { perror("pthread_create()"); } // 清理資源,并解鎖 thread->set_osthread(NULL); delete osthread; if (lock) os::Linux::createThread_lock()->unlock(); return false; } // 創(chuàng)建成功會將底層線程的ID保存在tid中 osthread->set_pthread_id(tid); // 等待子線程創(chuàng)建完成或者終止 { 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(); } } // 線程的數(shù)目達到極限了 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 chain assert(state == INITIALIZED, "race condition"); return true; }
下面我們來看看pthread_create會執(zhí)行的回調(diào)函數(shù)java_start,這個方法是所有新創(chuàng)建的線程必走的流程。
static void *java_start(Thread *thread) { // 嘗試隨機化熱棧幀高速緩存行的索引,這有助于優(yōu)化擁有相同棧幀線程去互相驅(qū)逐彼此的緩存行時,線程 // 可以是同一個JVM實例或者不同的JVM實例,這尤其有助于擁有超線程技術(shù)的處理器。 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 above if (!_thread_safety_check(thread)) { // notify parent thread MutexLockerEx 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 thread os::Linux::hotspot_sigmask(thread); // initialize floating point control register os::Linux::init_thread_fpu_state(); // handshaking with parent thread { MutexLockerEx ml(sync, Mutex::_no_safepoint_check_flag); // 設(shè)置為已經(jīng)初始化完成,并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,因此會調(diào)用JavaThread#run()方法 thread->run(); return 0; } void JavaThread::run() { // 初始化本地線程分配緩存(TLAB)相關(guān)的屬性 this->initialize_tlab(); // used to test validitity of stack trace backs this->record_base_of_stack_pointer(); // Record real stack base and size. this->record_stack_base_and_size(); // Initialize thread local storage; set before calling MutexLocker this->initialize_thread_local_storage(); this->create_stack_guard_pages(); this->cache_global_variables(); // 將線程的狀態(tài)更改為_thread_in_vm,線程已經(jīng)可以被VM中的安全點相關(guān)的代碼處理了,也就是說必須 // JVM如果線程在執(zhí)行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 computed thread_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_entry this->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對于那些需要特殊處理的類、方法等的聲明,我的理解就是一個方法表,根據(jù)下面這行代碼可以看出來,其實調(diào)用的就是run()方法.
/* common method and field names */ template(run_method_name, "run")
然后我們回到JVM_StartThread方法中,這里會接著調(diào)用prepare()方法,設(shè)置線程優(yōu)先級(將Java中的優(yōu)先級映射到os中),然后添加到線程隊列中去.最后會調(diào)用Thread::start(native_thread);
啟動線程。
void Thread::start(Thread* thread) { trace("start", thread); // start和resume不一樣,start被synchronized修飾 if (!DisableStartThread) { if (thread->is_Java_thread()) { // 在啟動線程之前初始化線程的狀態(tài)為RUNNABLE,為啥不能在之后設(shè)置呢?因為啟動之后可能是 // 等待或者睡眠等其他狀態(tài),具體是什么我們不知道 java_lang_Thread::set_thread_status(((JavaThread*)thread)->threadObj(), java_lang_Thread::RUNNABLE); } os::start_thread(thread); } }總結(jié)
一個Java線程對應(yīng)一個JavaThread->OSThread -> Native Thread
在調(diào)用java.lang.Thread#start()方法之前不會啟動線程,僅僅調(diào)用run()方法只是會在當前線程運行而已
//todo
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/70152.html
摘要:為了減少在中創(chuàng)建的字符串的數(shù)量,字符串類維護了一個字符串常量池。但是當執(zhí)行了方法后,將指向字符串常量池中的那個字符串常量。由于和都是字符串常量池中的字面量的引用,所以。究其原因,是因為常量池要保存的是已確定的字面量值。 String,是Java中除了基本數(shù)據(jù)類型以外,最為重要的一個類型了。很多人會認為他比較簡單。但是和String有關(guān)的面試題有很多,下面我隨便找兩道面試題,看看你能不能...
摘要:同理,若為,返回的結(jié)果若為或者,且為,返回的結(jié)果。同理,若為或者,且為,返回的結(jié)果是對象轉(zhuǎn)換基本類型的方法??磦€例子根據(jù)上述規(guī)則來解析為,上式為第條上式為第條上式為,上式為第條上式為 前不久因為一個項目設(shè)計的問題,煩心了好幾天,為了不留坑擁抱強類型語言特點,還是選擇了===作為數(shù)據(jù)判斷是否相等,對于==今天來探究一下貓膩(弱類型的JavaScript的坑真的太多了,typescript...
摘要:在表單提交時,瀏覽器會自動進行一系列的校驗工作,沒有通過校驗的表單是無法提交到服務(wù)器的。而方法提交表單,會在請求中發(fā)送表單字段鍵值對。表單提交事件表單提交到服務(wù)器時,會觸發(fā)事件。 上一篇主要溫習(xí)了一下form表單的屬性和表單元素,這一片主要講解用JavaScript如何操作form。 表單操作 取值 用JavaScript操作表單,免不了會有取值賦值操作,比如有以下表單: ...
摘要:用于或元素時,將提交指定的表單示例代碼只能上傳圖片只能上傳視頻使用的屬性,是一個類型的值,或文件后綴名。在以前,要想改變表單元素外觀,需要通過其他標簽來模擬。以下點到名的表單元素,還是可以照常使用的。 做為html中最為常見,應(yīng)用最廣泛的標簽之一,form常伴隨前端左右。了解更深,用的更順。 表單屬性 這個表單展示了form表單常用的屬性 屬性名 屬性值 ...
閱讀 3754·2021-09-22 15:49
閱讀 3310·2021-09-08 09:35
閱讀 1426·2019-08-30 15:55
閱讀 2328·2019-08-30 15:44
閱讀 719·2019-08-29 16:59
閱讀 1605·2019-08-29 16:16
閱讀 485·2019-08-28 18:06
閱讀 898·2019-08-27 10:55