Android Handler機制1之Thread

Android Handler機制系列文章整體內容如下:

本篇文章的主要內容如下:

  • 1、Java線程概念
  • 2、Android線程的實現
  • 3、線程的阻塞
  • 4、關于線程上下文切換
  • 5、關于線程的安全問題
  • 6、守護線程
  • 7、線程的內存

一、線程概念

(一)、進程

在現代的操作系統中,進程是資源分配的最小單位,而線城是CPU調度的基本單位。一個進程中最少有一個線程,名叫主線程。進程是程序執行的一個實例,比如說10個用戶同時執行Chrome瀏覽器,那么就有10個獨立的進程(盡管他們共享一個可執行代碼)。

1、進程的特點:

每一個進程都有自己的獨立的一塊內存空間、一組資源系統。其內部數據和狀態都是完全獨立的。進程的優點是提高CPU的運行效率,在同一個時間內執行多個程序,即并發執行。但是從嚴格上將,也不是絕對的同一時刻執行多個程序,只不過CPU在執行時通過時間片等調度算法不同進程告訴切換。

所以總結來說:進程由操作系統調度,簡單而且穩定,進程之間的隔離性好,一個進程的奔潰不會影響其他進程。單進程編程簡單,在多個情況下可以把進程和CPU進行綁定,從分利用CPU。

當然多進程也有一些缺點

2、進程的缺點:

一般來說進程消耗的內存比較大,進程切換代價很高,進程切換也像線程一樣需要保持上一個進程的上下文環境。比如在Web編程中,如果一個進程處理一個請求的話,如果提高并發量就要提高進程數,而進程數量受內存和切換代價限制。

(二)、線程

線程是進程的一個實體,是CPU調度和分配的基本單位,它比進程更下偶讀能獨立運行的基本單位,線程自己基本上不擁有系統資源,只擁有一點在運行中不可少的資源(如程序計數器、棧),但是它可與同屬一個進程的其他線程共享進程所擁有的全部資源。

同類的多線程共享一塊內存空間個一組系統資源,線程本身的數據通常只有CPU的寄存器數據,以及一個供程序執行的堆棧。線程在切換時負荷小,因此,線程也稱為輕負荷進程。一個進程中可以包含多個線程。

在JVM中,本地方法棧、虛擬機棧和程序計數器是線程隔離的,而堆區和方法區是線程共享的。

(三)、進程線程的區別

地址空間:線程是進程內的一個執行單元;進程至少有一個線程;一個進程內的多線程它們共享進程的地址空間;而進程自己獨立的地址空間
資源有用:進程是資源分配和擁有的單位,同一個進程內的線程共享進程的資源。
線程是處理器調度的基本單位,但進程不是
二者均可并發執行(下面補充了并發和并行的區別)

并發:多個事件在同一個時間段內一起執行
并行:多個事件在同一時刻同時執行

(四)、多線程

為了進一步提高CPU的利用率,多線程便誕生了。一個程序可以運行多個線程,多個線程可以同時執行,從整個應用角度上看,這個應用好像獨自擁有多個CPU一樣。雖然多線程進一步提高了應用的執行效率,但是由于線程之間會共享內存資源,這會導致一些資源同步的問題,另外,線程之間切換也會對資源有所消耗。

這里需要注意的是,如果一臺電腦只有一個CPU核心,那么多線也并沒有真正的"同時"運行,它們之間需要通過相互切換來共享CPU核心,所以,只有一個CPU核心的情況下,多線程不會提高應用效率。但是,現在計算機一般都會有多個CPU,并且每個CPU可能還有會多個核心,所以現在硬件資源條件下,多線程編程可以極大的提高應用的效率。

多CPU.png

在Java程序中,JVM負責線程的調度。線程調度是按照特定的機制為多個線程分配CPU的使用權。

調度的模式有兩種:分時調度和搶占式調度。分時調度是所有線程輪流獲得CPU使用權,并平均分配每個線程占用CPU的時間;搶占式調度是根據線程的優先級別來獲取CPU的使用權。JVM的線程調度模式采用了搶占式模式。

二、Android線程的實現

Android線程,一般地就是指Android虛擬機線程,而虛擬機線程是由通過系統調用而創建的Linux線程。純粹的的Linux線程與虛擬機線程區別在于虛擬機線程具有運行Java代碼的runtime。

在Android 中當擔也就對應一個類。從這一點上看Thread和其他類并沒有任何區別,只不過Thread的屬性和方法僅用于完成"線程管理"這個任務而已。在Android系統中,我們經常需要啟動一個新的線程,這些線程大多從Thread這個類繼承

(一)、Thread類

Thread.java

public class Thread implements Runnable {
  .....
}

通過上面代碼,我們可以知道Thread實現了Runnable,側面也說明線程是"可執行的代碼"。

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

Runnable是一個接口類,唯一提供的方法就是run()。

1、Thread的使用:

一般情況下,我們是這樣使用Thread的:

(1)、繼承Thread:
public  MyThread extends Thread{
}
MyThread  mt=new MyThread();
mt.start();
(2)、直接使用Runnable:

Thread的關鍵就是Runnable,因此下面的是另一個常見的用法。

new Thread(Runnable runnable).start();

2、Thread的常用方法:

Thread常用方法.png

3、Thread的常用字段:

    volatile ThreadGroup group;
    volatile boolean daemon;
    volatile String name;
    volatile int priority;
    volatile long stackSize;
    Runnable target;
    private static int count = 0;

    /**
     * Holds the thread's ID. We simply count upwards, so
     * each Thread has a unique ID.
     */
    private long id;

    /**
     * Normal thread local values.
     */
    ThreadLocal.Values localValues;

我們就依次說下:

  • group:每一個線程都屬于一個group,當線程被創建是,就會加入一個特定的group,當前程運行結束,會從這個group移除。
  • deamon:當前線程不是守護線程,守護線程只會在沒有非守護線程運行下才會運行
  • name:線程名稱
  • priority:線程優先級,Thread的線程優先級取值范圍為[1,10],默認優先級為5
  • stackSize:線程棧大小,默認是0,即使用默認的線程棧大小(由dalvik中的全局變量gDvm.stackSize決定)
  • target:一個Runnable對象,Thread的run()方法中會轉調target的run()方法,這是線程真正處理事務的地方。
  • id:線程id,通過遞增的count得到該id,如果沒有顯式給線程設置名字,那么久會使用Thread+id當前線程的名字。注意這里不是真正的線程id,即在logcat中打印的tid并不是這個id,那tid是指dalvik線程id
  • localValues:本地線程存儲(TLS)數據 關于TLS后面會詳細介紹

4、create()方法:

為什么要研究create()方法?因為Thread一種有9個構造函數,其中8個里面最終都是調用了create()方法

Thread.java 402行

 /**
     * Initializes a new, existing Thread object with a runnable object,
     * the given name and belonging to the ThreadGroup passed as parameter.
     * This is the method that the several public constructors delegate their
     * work to.
     *
     * @param group ThreadGroup to which the new Thread will belong
     * @param runnable a java.lang.Runnable whose method <code>run</code> will
     *        be executed by the new Thread
     * @param threadName Name for the Thread being created
     * @param stackSize Platform dependent stack size
     * @throws IllegalThreadStateException if <code>group.destroy()</code> has
     *         already been done
     * @see java.lang.ThreadGroup
     * @see java.lang.Runnable
     */
    private void create(ThreadGroup group, Runnable runnable, String threadName, long stackSize) {
        //步驟一 
        Thread currentThread = Thread.currentThread();

        //步驟二 
        if (group == null) {
            group = currentThread.getThreadGroup();
        }

        if (group.isDestroyed()) {
            throw new IllegalThreadStateException("Group already destroyed");
        }

        this.group = group;

        synchronized (Thread.class) {
            id = ++Thread.count;
        }

        if (threadName == null) {
            this.name = "Thread-" + id;
        } else {
            this.name = threadName;
        }

        this.target = runnable;
        this.stackSize = stackSize;

        this.priority = currentThread.getPriority();

        this.contextClassLoader = currentThread.contextClassLoader;

        // Transfer over InheritableThreadLocals.
        if (currentThread.inheritableValues != null) {
            inheritableValues = new ThreadLocal.Values(currentThread.inheritableValues);
        }

        // add ourselves to our ThreadGroup of choice
        //步驟二 
        this.group.addThread(this);
    }

我把create內部代碼大體上分為3個部分

  • 步驟一:通過靜態函數currentThread獲取創建線程所在的當前線程
  • 步驟二:將創新線程所在的當前線程的一些屬性賦值給即將創建的線程
  • 步驟三:通過調用ThreadGroup的addThread方法將新線程添加到group中。

4、Thread的生命周期:

線程共有6種狀態;在某一時刻只能是這6種狀態之一。這些狀態由Thread.State這個枚舉類型表示,并且可以通過getState()方法獲得當前具體的狀態類型。

Thread.State這個枚舉類在在Thread.java 78行

  /**
     * A representation of a thread's state. A given thread may only be in one
     * state at a time.
     */
    public enum State {
        /**
         * The thread has been created, but has never been started.
         */
        NEW,
        /**
         * The thread may be run.
         */
        RUNNABLE,
        /**
         * The thread is blocked and waiting for a lock.
         */
        BLOCKED,
        /**
         * The thread is waiting.
         */
        WAITING,
        /**
         * The thread is waiting for a specified amount of time.
         */
        TIMED_WAITING,
        /**
         * The thread has been terminated.
         */
        TERMINATED
    }

我們在用說明下:

  • NEW : 起勁尚未啟動的線程的狀態。當使用new一個新線程時,如new Thread(runnable),但還沒有執行start(),線程還有沒有開始運行,這時線程的狀態就是NEW。
  • RUNNABLE:可運行線程的線程狀態。此時的線程可能正在運行,也可能沒有運行。
  • BLOCKED:受阻塞并且正在等待監視鎖的某一線程的線程狀態。
    進入阻塞狀態的情況:
    • ① 等待某個操作的返回,例如IO操作,該操作返回之前,線程不會繼續后面的代碼
    • ② 等待某個"鎖",在其他線程或程序釋放這個"鎖"之前,線程不會繼續執行。
    • ③ 等待一定的觸發條件
    • ④ 線程執行了sleep()方法
    • ⑤ 線程被suspend()方法掛起
      一個被阻塞的線程在下列情況下會被重新激活
    • ① 執行了sleep(),隨眠時間已到
    • ② 等待的其他線程或者程序持有"鎖"已經被釋放
    • ③ 正在等待觸發條件的線程,條件已得到滿足
    • ④ 執行suspend()方法,被調用了resume()方法
    • ⑤ 等待的操作返回的線程,操作正確返回。
  • WAITING:某一等待線程的線程狀態。線程因為調用了Object.wait()或者Thread.join()而未運行,就會進入WAITING狀態。
  • TIMED_WAITING:具有指定等待時間的某一等待線程的線程狀態。線程是應為調用了Thread.sleep(),或者加上超時值在調用Object.wait()或者Thread.join()而未運行,則會進入TIMED_WAITING狀態。
  • TERMINATED:已終止線程狀態。線程已運行完畢,它的run()方法已經正常結束或者通過拋出異常而技術。線程的終止,run()方法結束,線程就結束了。

總結稱為一幅圖就是下圖


Thread的生命周期.png

5、線程的啟動:

上面說的這兩種方法獲取Thread,最終都通過start()方法啟動。

代碼在Thread.java 1058行

    /**
     * Starts the new Thread of execution. The <code>run()</code> method of
     * the receiver will be called by the receiver Thread itself (and not the
     * Thread calling <code>start()</code>).
     *
     * @throws IllegalThreadStateException - if this thread has already started.
     * @see Thread#run
     */
    public synchronized void start() {
         //保證線程只啟動一次
        checkNotStarted();

        hasBeenStarted = true;

        nativeCreate(this, stackSize, daemon);
    }


    private void checkNotStarted() {
        if (hasBeenStarted) {
            throw new IllegalThreadStateException("Thread already started");
        }
    }

通過上面代碼我們看到,start()方法里面首先是判斷是不是啟動過,如果沒啟動過直接調用nativeCreate(Thread , long, boolean)方法,通過方法名,我們知道是一個nativce方法

代碼在Thread.java 1066行

  private native static void nativeCreate(Thread t, long stackSize, boolean daemon);
5.1、nativeCreate()函數

nativeCreate()這是一個native方法,那么其所對應的JNI方法在哪里?在java_lang_Thread.cc中國getMethods是一個JNINativeMethod數據

代碼在java_lang_Thread.cc 179行

static JNINativeMethod gMethods[] = {
  NATIVE_METHOD(Thread, currentThread, "!()Ljava/lang/Thread;"),
  NATIVE_METHOD(Thread, interrupted, "!()Z"),
  NATIVE_METHOD(Thread, isInterrupted, "!()Z"),
  NATIVE_METHOD(Thread, nativeCreate, "(Ljava/lang/Thread;JZ)V"),
  NATIVE_METHOD(Thread, nativeGetStatus, "(Z)I"),
  NATIVE_METHOD(Thread, nativeHoldsLock, "(Ljava/lang/Object;)Z"),
  NATIVE_METHOD(Thread, nativeInterrupt, "!()V"),
  NATIVE_METHOD(Thread, nativeSetName, "(Ljava/lang/String;)V"),
  NATIVE_METHOD(Thread, nativeSetPriority, "(I)V"),
  NATIVE_METHOD(Thread, sleep, "!(Ljava/lang/Object;JI)V"),
  NATIVE_METHOD(Thread, yield, "()V"),
};

其中一項為:

NATIVE_METHOD(Thread, nativeCreate, "(Ljava/lang/Thread;JZ)V"),

這里的NATIVE_METHOD定義在java_lang_Object.cc文件中,如下:

代碼在java_lang_Object.cc 25行

#define NATIVE_METHOD(className, functionName, signature, identifier) \
    { #functionName, signature, reinterpret_cast<void*>(className ## _ ## identifier) }

將宏定義展開并帶入,可得所對應的方法名為Thread_nativeCreate

5.2、Thread_nativeCreate()函數

代碼在java_lang_Thread.cc 49行

static void Thread_nativeCreate(JNIEnv* env, jclass, jobject java_thread, jlong stack_size,
                                jboolean daemon) {
  Thread::CreateNativeThread(env, java_thread, stack_size, daemon == JNI_TRUE);
}

看到 只是調用了Thread類的CreateNativeThread

5.3、Thread::CreateNativeThread()函數

代碼在thread.cc 388行

void Thread::CreateNativeThread(JNIEnv* env, jobject java_peer, size_t stack_size, bool is_daemon) {
  CHECK(java_peer != nullptr);
  Thread* self = static_cast<JNIEnvExt*>(env)->self;
  Runtime* runtime = Runtime::Current();

  // Atomically start the birth of the thread ensuring the runtime isn't shutting down.
  bool thread_start_during_shutdown = false;
  {
    MutexLock mu(self, *Locks::runtime_shutdown_lock_);
    if (runtime->IsShuttingDownLocked()) {
      thread_start_during_shutdown = true;
    } else {
      runtime->StartThreadBirth();
    }
  }
  if (thread_start_during_shutdown) {
    ScopedLocalRef<jclass> error_class(env, env->FindClass("java/lang/InternalError"));
    env->ThrowNew(error_class.get(), "Thread starting during runtime shutdown");
    return;
  }

  Thread* child_thread = new Thread(is_daemon);
  // Use global JNI ref to hold peer live while child thread starts.
  child_thread->tlsPtr_.jpeer = env->NewGlobalRef(java_peer);
  stack_size = FixStackSize(stack_size);

  // Thread.start is synchronized, so we know that nativePeer is 0, and know that we're not racing to
  // assign it.
  env->SetLongField(java_peer, WellKnownClasses::java_lang_Thread_nativePeer,
                    reinterpret_cast<jlong>(child_thread));

  // Try to allocate a JNIEnvExt for the thread. We do this here as we might be out of memory and
  // do not have a good way to report this on the child's side.
  std::unique_ptr<JNIEnvExt> child_jni_env_ext(
      JNIEnvExt::Create(child_thread, Runtime::Current()->GetJavaVM()));

  int pthread_create_result = 0;
  if (child_jni_env_ext.get() != nullptr) {
    pthread_t new_pthread;
    pthread_attr_t attr;
    child_thread->tlsPtr_.tmp_jni_env = child_jni_env_ext.get();
    CHECK_PTHREAD_CALL(pthread_attr_init, (&attr), "new thread");
    CHECK_PTHREAD_CALL(pthread_attr_setdetachstate, (&attr, PTHREAD_CREATE_DETACHED),
                       "PTHREAD_CREATE_DETACHED");
    CHECK_PTHREAD_CALL(pthread_attr_setstacksize, (&attr, stack_size), stack_size);

     /***這里是重點,創建線程***/
    pthread_create_result = pthread_create(&new_pthread,
                                           &attr,
                                           Thread::CreateCallback,
                                           child_thread);
    CHECK_PTHREAD_CALL(pthread_attr_destroy, (&attr), "new thread");

    if (pthread_create_result == 0) {
      // pthread_create started the new thread. The child is now responsible for managing the
      // JNIEnvExt we created.
      // Note: we can't check for tmp_jni_env == nullptr, as that would require synchronization
      //       between the threads.
      child_jni_env_ext.release();
      return;
    }
  }

  // Either JNIEnvExt::Create or pthread_create(3) failed, so clean up.
  {
    MutexLock mu(self, *Locks::runtime_shutdown_lock_);
    runtime->EndThreadBirth();
  }
  // Manually delete the global reference since Thread::Init will not have been run.
  env->DeleteGlobalRef(child_thread->tlsPtr_.jpeer);
  child_thread->tlsPtr_.jpeer = nullptr;
  delete child_thread;
  child_thread = nullptr;
  // TODO: remove from thread group?
  env->SetLongField(java_peer, WellKnownClasses::java_lang_Thread_nativePeer, 0);
  {
    std::string msg(child_jni_env_ext.get() == nullptr ?
        "Could not allocate JNI Env" :
        StringPrintf("pthread_create (%s stack) failed: %s",
                                 PrettySize(stack_size).c_str(), strerror(pthread_create_result)));
    ScopedObjectAccess soa(env);
    soa.Self()->ThrowOutOfMemoryError(msg.c_str());
  }
}

這里面重點是pthread_create()函數,pthread_create是pthread庫中的函數,通過syscall再調用到clone來創建線程。

  • 原型:int pthread_create((pthred_t thread,pthread_attr_t * attr, void * (start_routine) (void * ), void * arg))
  • 頭文件:#include
  • 入參: thread(線程標識符)、attr(線程屬性設置)、start_routine(線程函數的起始地址)、arg(傳遞給start_rountine參數):
  • 返回值:成功則返回0;失敗則返回-1
  • 功能:創建線程,并調用線程其實地址指向函數start_rountine。

再往下就到內核層了,由于篇幅限制,就先不深入,有興趣的同學可以自行研究

三、線程的阻塞

線程阻塞指的是暫停一個線程的執行以等待某個條件發生(如某資源就緒)。Java提供了大量的方法來支持阻塞,下面讓我們逐一分析。

(一)、sleep()方法

sleep()允許指定以毫米為單位的一段時間作為參數,它使得線程在指定的時間內進入阻塞狀態,不能得到CPU時間,指定的時間已過,線程重新進入可執行狀態。典型地,sleep()被用在等待某個資源就緒的情形:測試發現條件不滿足后,讓線程阻塞一段后重新測試,直到條件滿足為止。

(二) suspend()和resume()方法

兩個方法配套使用,suspend()使得線程進入阻塞狀態,并且不會自動恢復,必須其對應的resume()被調用,才能使得線程重新進入可執行狀態。典型地,suspend()和resume()被用在等待另一個線程產生的結果的情形:測試發現結果還沒有產生后,讓線程阻塞,另一個線程產生了結果后,調用resume()使其恢復。

(三) yield()方法

yeield()使得線程放棄當前分得的CPU時間,但是不使線程阻塞,即線程仍處于可執行狀態,隨時可能再次分的CPU時間。調用yield()效果等價于調度程度認為該線程已執行了足夠的時間從而轉到另一個線程。

(四) wait()和notify()方法

兩個方法配套使用,wait()使得線程進入阻塞狀態,它有兩種形式,一種允許指定以毫秒為單位的一段時間作為參數,另一種沒有慘呼是,當前對應的notify()被調用或者超出指定時間線程重新進入可執行狀態,后者則必須對應的notify()被調用。初看起來它們與suspend()和resume()方法對沒有什么分別,但是事實上它們是截然不同的。區別的核心在于,前面敘述的所有方法,阻塞時都不會釋放占用的鎖(如果占用的話),而這一對方法則相反。

這里需要重點介紹下wait()和notify()

  • 首先,前面敘述的所有方法都隸屬于Thread類,但是這一對卻直接隸屬于Object類,也就是說,所有對象都擁有這一對方法。初看起來十分不可思議,但是實際上卻是很自然的,因為這一對方法阻塞時需要釋放占用的鎖,而鎖是任何對象都具有的,調用對象wait()方法導致線程阻塞,并且該對象上的鎖釋放。而調用對象的notify()方法則導致因調用對象的wait()方法而阻塞線程中隨機選擇的一個解除阻塞(但要等待獲得鎖后才真正可執行)。
  • 其次,前面敘述的所有方法都可在任何位置調用,但是這一對方法卻必須在synchronized方法或塊中調用,理由也很簡單,只有synchronized方法或塊中當前線程才占有所,才有鎖可以釋放。同樣的道理,調用這一對方法的對象上的鎖必須為當前線程鎖擁有,這樣才有鎖可以釋放。因此,這一對方法調用必須防止在這樣的synchronized方法或者塊中,該方法或者塊的上鎖對象就是調用這對方法的對象。若不滿足這一條件,則程序雖然仍能編譯,但是在運行時會出現IllegalMonitorStateException異常。
  • 第三,調用notify()方法導致解除阻塞的線程是從因調用該對象的wait()方法而阻塞的線程中隨機選取的,我們無法預料那個一個線程將會被選擇,所以編程時需要特別小心,避免因這種不確定性而產生問題。
  • 最后,除了notify(),還有一個方法notifyAll()也可能其到類似作用,唯一的區別是在于,調用notifyAll()方法將把 因 調用該對象的wait()方法而阻塞的所有線程一次性全部解除阻塞。當然,只有獲得鎖的那一個線程才能進入可執行狀態。

四、關于線程上下文切換

在多線程編程中,多個線程公用資源,計算機會多各個線程進行調度。因此各個線程會經歷一系列不同的狀態,以及在不同的線程間進行切換。
既然線程需要被切換,在生命周期中處于各種狀態,如等待、阻塞、運行。吧線程就需要能夠保存線程,在線程被切換后/回復后需要繼續在上一個狀態運行。這就是所謂的上下文切換。為了實現上下文切換,勢必會消耗資源,造成性能損失。因為我們在進行多線程編程過程中需要減少上下文切換,提高程序運行性能。

一些常用的方法:

  • 無鎖并發編程:可以采用一些方法避免鎖的使用,如不同線程去處理不同段
  • CAS常用算法:原子操作不需要加鎖
  • 使用最少線程:避免創建不需要的線程
  • 協程:在單線程里實現多任務調度

五、關于線程的安全問題

線程安全無非是要控制多個線程對某個資源的有序訪問或修改。即多個線程,一個臨界區,通過通知這些線程對臨界區的訪問,使得每個線程的每次執行結果都相同(搞清楚這個問題,可以避免多線程編程的狠多錯誤)

(一)、實現線程安全的工具:

  • 1 隱式鎖:synchronized
  • 2 顯式鎖:java.util.concurrent.lock
  • 3 關鍵字:volatile
  • 4 原子操作:java.util.concurrent.atomic

(二)、線程同步閥:

  • 1 CountDownLatch類是一個同步計數器。(java.util.concurrent.CountDownLatch)
  • 2 CyclicBarrier是一個同步輔助類,它允許一組線程相互等待,直到到到某個公共屏蔽點(common barrier point)。在涉及一組固定大小的線程的程序中,這些線程不時地相互等待,這時候CyclicBarrier很有用。因為barrier在釋放等待線程后可以重用,因此稱為循環的barrier。
  • 3 信號量(java.util.concurrent.Semaphone),計數信號量,該信號量維護一定大小的許可集合,規定最多允許多少個進程同時訪問臨界區。其中,semp.acquire()類似于操作系統中的P操作,semp.release類似于操作系統的V操作。
  • 4 任務機制(java.util.concurrent.Future->FutureTask)。結合Runnable使用!一般FutureTask多用于耗時的計算,主線程在完成自己的任務后再去獲取結果;只有計算完成時獲取,否則一直阻塞。

六、守護線程

(一) 概念

守護線程我覺得還是很有用的。首先看看守護進程是什么?守護線程就是后臺運行的線程。普通線程結束后,守護線程自動結束。一般main線程視為守護線程,以及GC、數據庫連接池等,都做成守護進程。

(二) 特點

守護線程就像備胎一樣,JRE(女神)根本不會管守護進行有沒有,在不在,只要前臺線程結束,就算執行完畢了。

(三) 如何使用

直接調用setDeamon() 即可。

(四) 注意事項

setDaemon(true) 必須在start()方法之前調用;在守護線程中開啟的線程也是守護線程;守護線程中不能進行I/O操作。

七、線程的內存

(一) Java內存模型

Java內存模型規范了Java虛擬機與計算機內存是符合協同工作的。Java虛擬機是一個完整的計算機的一個模型,因此這個模型自然也包含一個內存模型——又稱為Java內存模型。

如果你想設計表現良好的并發程序,理解Java內存模型是非常重要的。Java內存模型規定了如何和何時可以看到由其他線程修改過后的共享變量的值,以及在必須時如何同步的訪問共享變量。

原始的Java內存模型存在一些不足,因此Java內存模型在Java 1.5時被重新修訂。這個版本的Java內存模型在Java8中仍在使用。

(二) Java 線程內存模型原理

Java內存模型把Java虛擬機內部劃分為線程棧和堆。下面這張圖演示了Java內存模型的邏輯視圖。

Java內存模型原理.png
  • 每一個運行在Java虛擬機李的線程都擁有自己的線程棧。這個線程棧包含了這個線程調用的方法當前執行點相關的信息。一個線程僅能訪問自己的線程棧。一個線程創建的本地變量對其他線程不可見,僅自己可見。即使兩個線程執行同樣的代碼,這兩個線程仍然在自己的線程棧中的代碼來創建本地變量。因此,每個線程擁有每個本地變量的獨有版本。
  • 所有原始類型的本地變量都存放在線程棧上,因此對其他線程不可見。一個線程可能向另一個線程傳遞一個原始類型變量的拷貝,但是它不能共享這個原始類型變量自身。
  • 堆上包含在Java程序中創建的所有對象,無論是哪一個對象創建的。這包括原始類型的對象版本。如果一個對象被創建然后賦值給一個局部變量,或者用來作為另一個對象的成員變量,這個對象仍然是存在堆上。

下面這張圖演示了調用棧和本地變量存放在線程棧上,對象存放在堆上。

堆與棧.png

所以大體可以分為以下幾種情況:

  • 一個本地變量可能是原始類型,在這種情況下,它總是"待在"線程棧上。
  • 一個本地變量也可能是指向一個對象的一個引用。在這種情況下,引用(這個本地變量)存放在這個線程棧上,但是對象本身存放在堆上。
  • 一個對象可能包含方法,這些方法可能包含本地變量。這些本地變量仍然存放在線程棧上,即使這些方法是所屬的對象存放在堆上。
  • 一個對象的成員變量可能隨著這個對象自身存放在堆上。不管這個成員變量是原始類型還是引用類型。
  • 靜態成員變量跟隨著類定義一起也存放在堆上。
  • 存放在堆上的對象可以被所有吃持有這個對象引用的線程訪問。當一個線程可以訪問一個對象時,它也可以訪問這個對象的成員變量。如果兩個線程同時調用同一個對象上的同一個方法,它們將會都訪問這個對象的成員變量,但是每一個線程都擁有這個本地變量的私有拷貝。

下圖演示了上面提到的點:

情況.png

PS:

  • 1、兩個線程擁有一系列的本地變量。其中一個本地變量(Local Variable 2)執行堆上的一個共享對象(Object 3)。這兩個線程分貝擁有同一個對象的不同引用。這些引用都是本地變量,因此存放在各自線程的線程棧上。這兩個不同的引用指向堆上同一個對象。
  • 2、這個共享對象(Object 3)持有Object 2和Object 4一個引用作為其成員員變量(如圖中Object 3 指向 Object 2和Object 4的箭頭)。通過這在Object 3中這些成員變量引用,這兩個線程就可以訪問到Object 2和 Object 4。

上面這張圖也展示了指向堆上兩個不同對象的一個本地變量。在這種情況下,指向兩個不同對象的引用不是同一個對象。理論上,兩個線程都可以訪問Object 1 和Object 5,如果兩個線程都擁有兩個對象的引用。但是在上圖中,每個線程僅有一個引用指向兩個對象其中之一。

(三) 硬件內存架構

現代硬件內存模型與Java模型有一些不同。理解內存模型結構以及Java內存模型如何與它協同工作也是非常重要的。這部分描述了通用的硬件內存架構,下面的部分將會描述內存是如何與它"聯合"工作的。

現代計算機硬件架構的簡單圖示:

現代計算機硬件架構.png
  • 一個現代計算機通常由兩個或者多個CPU。其中一些CPU還有多核。從這一點可以看出,在一個或者多個CPU的現代計算上運行多個線程是可能的。每個CPU在某一時刻運行一個線程是沒有問題的。這意味著,如果你的Java程序是多線程的,在你的Java程序中每個CPU上一個線程可能同時(并發)執行。
  • 每個CPU都包含一系列的寄存器,它們是CPU內存的基礎。CPU在寄存器上執行操作的速度遠大于主存上執行的速度。這是因為CPU訪問寄存器的速度遠大于主存。
  • 每個CPU可能還有一個CPU緩存層。實際上,絕地多數的現代CPU都有一定大小的緩存層。CPU訪問緩存層的速度快于訪問主存的速度,但通常比訪問內部寄存器的速度還要慢一點。一些CPU還有多層緩存,但這些對理解Java內存模型如何和內存交互不是那么重要。只要知道CPU中可以由一個緩存層就可以了。
  • 一個計算機還包含一個主存。所有CPU都可以訪問主存。主存通常比CPU緩存大得多。
  • 通常情況下,當一個CPU需要讀取主存時,它會將主存的部分讀到CPU緩存中。它甚至可能將緩存中的部分內容讀到它的內部寄存器中,然后在寄存器中執行操作。當CPU需要將結果寫回到主存中時,它會將內部寄存器值刷新到緩存中,然后在某個時間點將值刷新回主存。
  • 當CPU需要在緩存層存放一些東西的時候,存放在緩存中的內容通常會刷新回主存。CPU緩存可以在某一時刻將數據局部寫到它的內存中,和在某一時刻局部刷新它的內存。它不會在某一時刻讀/寫整個緩存。通常,在一個被稱作"cache lines"的更小內存塊中緩存被更新。一個或者多個緩存行可能被讀到緩存,一個或者多個緩存行可能再被刷新回主存。

(四) Java內存模型和硬件內存架構之間的橋接

上面已經提到,Java內存模型與硬件內存架構之間存在差異。硬件內存架構沒有區分線程棧和堆。對于硬件,所有的線程棧和堆都分布在主內中。部分線程棧和堆可能有時候會出現在CPU緩存中和CPU內部的寄存器中。

如下圖所示:

橋接.png

當對象和變量被存放在計算機中各個不同的內存區域中時,就可能會出現一些具體的問題。主要包含兩個方面:

  • 線程對共享變量修改的可見性
  • 當讀、寫和檢查共享變量時rece conditions(競爭條件)

下面我們專門來解釋一下上面的兩個問題

1、對象的可見性

如果兩個或者更多的線程在沒有正確使用volatile聲明或者同步的情況下共享一個對象,一個線程更新這個共享對象可能對其他線程來說是不可見的。

想象一下,共享對象那個被初始化在主存中。跑在CPU上的一個線程將這個共享對象讀到CPU緩存中。然后修改了這個對象。要CPU緩存沒有被刷新到駐村,對象修改后的版本對跑在其他CPU上的線程都是不可見的。這種方式可能導致每個線程擁有這個共享對象的私有拷貝,每個拷貝停留在不同的CPU緩存中。

下面示意了這種情形。

對象的可見性.png

跑在左邊的CPU的線程拷貝這個共享對象到它的CPU緩存中,然后將count變量的值修改為2,這個修改對跑在右邊的CPU上的其他線程是不可見的,因為修改后count的值還沒有被刷新回主存中去。

解決這個問題你可以使用volatile關鍵字。volatile關鍵字可以保證直接從主存中讀取一個變量,如果這個變量被修改后,總是會寫回到主存中去。

2、Race Conditions(競爭條件)

如果兩個或者更多的線程共享一個對象,多個線程在這個共享對象上更新變量,就可能放生Race Conditions(競爭條件)。想象一下,如果線程A讀取一個共享對象的變量count到它的CPU緩存中。再想象一下,線程B也做了同樣的事情,但是往一個不同的CPU緩存個中。現在線程A將count加1,線程B也做了同樣的事情,現在count已經被增加了兩個,每個CPU緩存中一次。如果這些增加操作被順序執行,變量count應該被增加兩次,然后原值+2倍寫回到主存中區。然而,兩次增加都是在沒有適當的同步下并發執行的。無論線程A還是線程B將count修改后的版本寫回到主存中去,修改后的值僅會被原值大1,盡管增加了兩次。

下圖演示了上面描述的情況:

Race Conditions.png

解決這個問題可以使用Java同步塊。一個同步塊可以保證在同一時刻僅有一個線程可以進入代碼的臨界區。同步塊還可以保證代碼塊中所有被訪問的變量將會從主存中讀入,當線程退出同步代碼塊是,所有被更新的變量會被刷新回主存中區,不管這個變量是否被聲明為volatile。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,119評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,382評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,038評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,853評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,616評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,112評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,192評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,355評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,869評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,727評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,928評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,467評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,165評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,570評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,813評論 1 282
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,585評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,892評論 2 372

推薦閱讀更多精彩內容

  • 從三月份找實習到現在,面了一些公司,掛了不少,但最終還是拿到小米、百度、阿里、京東、新浪、CVTE、樂視家的研發崗...
    時芥藍閱讀 42,316評論 11 349
  • Java8張圖 11、字符串不變性 12、equals()方法、hashCode()方法的區別 13、...
    Miley_MOJIE閱讀 3,720評論 0 11
  • 1. Java基礎部分 基礎部分的順序:基本語法,類相關的語法,內部類的語法,繼承相關的語法,異常的語法,線程的語...
    子非魚_t_閱讀 31,711評論 18 399
  • 下面是我自己收集整理的Java線程相關的面試題,可以用它來好好準備面試。 參考文檔:-《Java核心技術 卷一》-...
    阿呆變Geek閱讀 14,883評論 14 507
  • 告訴大家一個好消息:我快要換工作了。 呵呵!對我來說是個好消息。 我要去做人力資源工作了。說白了,就是幫人家廠里面...
    越來越好伍新國閱讀 364評論 2 3