天天看點

并發,又是并發

并發,又是并發

三豐 soft張三豐

并發,又是并發

在 java 中守護線程和本地線程差別

java 中的線程分為兩種:守護線程(Daemon)和使用者線程(User)。任何線程都可以設定為守護線程和使用者線程,通過方法 Thread.setDaemon(boolon);true 則把該線程設定為守護線程,反之則為使用者線程。Thread.setDaemon()必須在 Thread.start()之前調用,否則運作時會抛出異常。

兩者的差別:唯一的差別是判斷虛拟機(JVM)何時離開,Daemon 是為其他線程提供服務,如果全部的 User Thread 已經撤離,Daemon 沒有可服務的線程,JVM 撤離。也可以了解為守護線程是 JVM 自動建立的線程(但不一定),使用者線程是程式建立的線程;比如 JVM 的垃圾回收線程是一個守護線程,當所有線程已經撤離,不再産生垃圾,守護線程自然就沒事可幹了,當垃圾回收線程是 Java 虛拟機上僅剩的線程時,Java 虛拟機會自動離開。擴充:Thread Dump 列印出來的線程資訊,含有 daemon 字樣的線程即為守護程序,可能會有:服務守護程序、編譯守護程序、windows 下的監聽 Ctrl+break的守護程序、Finalizer 守護程序、引用處理守護程序、GC 守護程序。

并發,又是并發

什麼是多線程中的上下文切換?

多線程會共同使用一組計算機上的 CPU,而線程數大于給程式配置設定的 CPU 數量時,為了讓各個線程都有執行的機會,就需要輪轉使用 CPU。不同的線程切換使用 CPU發生的切換資料等就是上下文切換。

  • 若目前線程還在運作而時間片結束後,CPU将被剝奪并配置設定給另一個線程。
  • 若線程在時間片結束前阻塞或結束,CPU進行線程切換。而不會造成CPU資源浪費。

對比串聯執行和并發執行

``` java?linenums
`    private static final long count = 1000000;

public static void main(String[] args) throws Exception {
    concurrency();
    series();
}
/**
 * 并發執行
 * @throws Exception
 */
private static void concurrency() throws Exception {
    long start = System.currentTimeMillis();
    //建立線程執行a+=
    Thread thread = new Thread(new Runnable() {
        public void run() {
            int a = 0;
            for (int i = 0; i < count; i++) {
                a += 1;
            }
        }
    });
    //啟動線程執行
    thread.start();
    //使用主線程執行b--;
    int b = 0;
    for (long i = 0; i < count; i++) {
        b--;
    }
    //合并線程,統計時間
    thread.join();
    long time = System.currentTimeMillis() - start;
    System.out.println("Concurrency:" + time + "ms, b = " + b);
}
/**
 * 串聯執行
 */
private static void series() {
    long start = System.currentTimeMillis();
    int a = 0;
    for (long i = 0; i < count; i++) {
        a += 1;
    }
    int b = 0;
    for (int i = 0; i < count; i++) {
        b--;
    }
    long time = System.currentTimeMillis() - start;
    System.out.println("Serial:" + time + "ms, b = " + b + ", a = " + a);
};
           

通過修改循環次數,對比串行運作和并發運作的時間測試結果:

并發,又是并發

通過資料的對比我們可以看出。在一萬以下的循環次數時,串聯的執行速度比并發的執行速度塊。是因為線程上下文切換導緻額外的開銷。

死鎖與活鎖的差別,死鎖與饑餓的差別?

死鎖:是指兩個或兩個以上的程序(或線程)在執行過程中,因争奪資源而造成的一種互相等待的現象,若無外力作用,它們都将無法推進下去。産生死鎖的必要條件:

  • 互斥條件:所謂互斥就是程序在某一時間内獨占資源。
  • 請求與保持條件:一個程序因請求資源而阻塞時,對已獲得的資源保持不放。
  • 不剝奪條件:程序已獲得資源,在末使用完之前,不能強行剝奪。
  • 循環等待條件:若幹程序之間形成一種頭尾相接的循環等待資源關系。活鎖:任務或者執行者沒有被阻塞,由于某些條件沒有滿足,導緻一直重複嘗試,失敗,嘗試,失敗。活鎖和死鎖的差別在于,處于活鎖的實體是在不斷的改變狀态,所謂的“活”, 而處于死鎖的實體表現為等待;活鎖有可能自行解開,死鎖則不能。

    饑餓:一個或者多個線程因為種種原因無法獲得所需要的資源,導緻一直無法執行的狀态。Java 中導緻饑餓的原因:

  • 高優先級線程吞噬所有的低優先級線程的 CPU 時間。
  • 線程被永久堵塞在一個等待進入同步塊的狀态,因為其他線程總是能在它之前持續地對該同步塊進行通路。
  • 線程在等待一個本身也處于永久等待完成的對象(比如調用這個對象的 wait 方法),因為其他線程總是被持續地獲得喚醒。
  • Java 中用到的線程排程算法是什麼?采用時間片輪轉的方式。可以設定線程的優先級,會映射到下層的系統上面的優先級上,如非特别需要,盡量不要用,防止線程饑餓。

Java中Runnable和Callable有什麼不同?

Runnable和Callable都代表那些要在不同的線程中執行的任務。Runnable從JDK1.0開始就有了,Callable是在 JDK1.5增加的。它們的主要差別是Callable的 call() 方法可以傳回值和抛出異常,而Runnable的run()方法沒有這些功能。Callable可以傳回裝載有計算結果的Future對象。

什麼是 Executors 架構?

Executor 架構是一個根據一組執行政策調用,排程,執行和控制的異步任務的架構。無限制的建立線程會引起應用程式記憶體溢出。是以建立一個線程池是個更好的的解決方案,因為可以限制線程的數量并且可以回收再利用這些線程。利用Executors 架構可以非常友善的建立一個線程池。

什麼是阻塞隊列?阻塞隊列的實作原理是什麼?如何使用阻塞隊列來實作生産者-消費者模型?

阻塞隊列常用于生産者和消費者的場景,生産者是往隊列裡添加元素的線程,消費者是從隊列裡拿元素的線程。阻塞隊列就是生産者存放元素的容器,而消費者也隻從容器裡拿元素。JDK7 提供了 7 個阻塞隊列。分别是:ArrayBlockingQueue :一個由數組結構組成的有界阻塞隊列。LinkedBlockingQueue :一個由連結清單結構組成的有界阻塞隊列。PriorityBlockingQueue :一個支援優先級排序的無界阻塞隊列。DelayQueue:一個使用優先級隊列實作的無界阻塞隊列。SynchronousQueue:一個不存儲元素的阻塞隊列。LinkedTransferQueue:一個由連結清單結構組成的無界阻塞隊列。LinkedBlockingDeque:一個由連結清單結構組成的雙向阻塞隊列。

什麼是 Callable 和 Future?

Callable 接口類似于 Runnable,從名字就可以看出來了,但是 Runnable 不會傳回結果,并且無法抛出傳回結果的異常,而 Callable 功能更強大一些,被線程執行後,可以傳回值,這個傳回值可以被 Future 拿到,也就是說,Future 可以拿到異步執行任務的傳回值。可以認為是帶有回調的 Runnable。Future 接口表示異步任務,是還沒有完成的任務給出的未來結果。是以說 Callable用于産生結果,Future 用于擷取結果。

Java 中你怎樣喚醒一個阻塞的線程?

在 Java 發展史上曾經使用 suspend()、resume()方法對于線程進行阻塞喚醒,但随之出現很多問題,比較典型的還是死鎖問題。解決方案可以使用以對象為目标的阻塞,即利用 Object 類的 wait()和 notify()方法實作線程阻塞。首 先 ,wait、notify 方法是針對對象的,調用任意對象的 wait()方法都将導緻線程阻塞,阻塞的同時也将釋放該對象的鎖,相應地,調用任意對象的 notify()方法則将随機解除該對象阻塞的線程,但它需要重新擷取改對象的鎖,直到擷取成功才能往下執行;其次,wait、notify 方法必須在 synchronized 塊或方法中被調用,并且要保證同步塊或方法的鎖對象與調用 wait、notify 方法的對象是同一個,如此一來在調用 wait 之前目前線程就已經成功擷取某對象的鎖,執行 wait 阻塞後目前線程就将之前擷取的對象鎖釋放。

Java 中用到的線程排程算法是什麼?

有兩種排程模型:分時排程模型和搶占式排程模型。

分時排程模型是指讓所有的線程輪流獲得 cpu 的使用權,并且平均配置設定每個線程占用的 CPU 的時間片這個也比較好了解。Java虛拟機采用搶占式排程模型,是指優先讓可運作池中優先級高的線程占用CPU,如果可運作池中的線程優先級相同,那麼就随機選擇一個線程,使其占用CPU。處于運作狀态的線程會一直運作,直至它不得不放棄 CPU。

notify()和 notifyAll()有什麼差別?

當一個線程進入 wait 之後,就必須等其他線程 notify/notifyall,使用 notifyall,可以喚醒所有處于 wait 狀态的線程,使其重新進入鎖的争奪隊列中,而 notify 隻能喚醒一個。如果沒把握,建議 notifyAll,防止 notigy 因為信号丢失而造成程式異常。

當一個線程進入某個對象的一個 synchronized 的執行個體方法後,其它線程是否可進入此對象的其它方法?

如果其他方法沒有 synchronized 的話,其他線程是可以進入的。

是以要開放一個線程安全的對象時,得保證每個方法都是線程安全的。

樂觀鎖和悲觀鎖的了解及如何實作,有哪些實作方式?

悲觀鎖:總是假設最壞的情況,每次去拿資料的時候都認為别人會修改,是以每次在拿資料的時候都會上鎖,這樣别人想拿這個資料就會阻塞直到它拿到鎖。傳統的關系型資料庫裡邊就用到了很多這種鎖機制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。再比如 Java 裡面的同步原語 synchronized 關鍵字的實作也是悲觀鎖。樂觀鎖:顧名思義,就是很樂觀,每次去拿資料的時候都認為别人不會修改,是以不會上鎖,但是在更新的時候會判斷一下在此期間别人有沒有去更新這個資料,可以使用版本号等機制。樂觀鎖适用于多讀的應用類型,這樣可以提高吞吐量,像資料庫提供的類似于 write_condition 機制,其實都是提供的樂觀鎖。在 Java中 java.util.concurrent.atomic 包下面的原子變量類就是使用了樂觀鎖的一種實作方式 CAS 實作的。

樂觀鎖的實作方式:

  • 使用版本辨別來确定讀到的資料與送出時的資料是否一緻。送出後修改版本辨別,不一緻時可以采取丢棄和再次嘗試的政策。
  • java 中的 Compare and Swap 即 CAS ,當多個線程嘗試使用 CAS 同時更新同一個變量時,隻有其中一個線程能更新變量的值,而其它線程都失敗,失敗的線程并不會被挂起,而是被告知這次競争中失敗,并可以再次嘗試。CAS 操作中包含三個操作數 —— 需要讀寫的記憶體位置(V)、進行比較的預期原值(A)和拟寫入的新值(B)。如果記憶體位置 V 的值與預期原值 A 相比對,那麼處理器會自動将該位置值更新為新值 B。否則處理器不做任何操作。

    CAS 缺點:

  • ABA 問題:比如說一個線程 one 從記憶體位置 V 中取出 A,這時候另一個線程 two 也從記憶體中取出 A,并且 two 進行了一些操作變成了 B,然後 two 又将 V 位置的資料變成 A,這時候線程 one 進行 CAS 操作發現記憶體中仍然是 A,然後 one 操作成功。盡管線程 one 的 CAS 操作成功,但可能存在潛藏的問題。從 Java1.5 開始 JDK 的 atomic包裡提供了一個類 AtomicStampedReference 來解決 ABA 問題。
  • 循環時間長開銷大:對于資源競争嚴重(線程沖突嚴重)的情況,CAS 自旋的機率會比較大,進而浪費更多的 CPU 資源,效率低于 synchronized。
  • 隻能保證一個共享變量的原子操作:當對一個共享變量執行操作時,我們可以使用循環 CAS 的方式來保證原子操作,但是對多個共享變量操作時,循環 CAS 就無法保證操作的原子性,這個時候就可以用鎖。

如何在兩個線程間共享資料?

在兩個線程間共享變量即可實作共享。

一般來說,共享變量要求變量本身是線程安全的,然後線上程内使用的時候,如果有對共享變量的複合操作,那麼也得保證複合操作的線程安全性。

什麼是 ThreadLocal 變量?

ThreadLocal 是 Java 裡一種特殊的變量。每個線程都有一個 ThreadLocal 就是每個線程都擁有了自己獨立的一個變量,競争條件被徹底消除了。它是為建立代價高昂的對象擷取線程安全的好方法,比如你可以用 ThreadLocal 讓SimpleDateFormat 變成線程安全的,因為那個類建立代價高昂且每次調用都需要建立不同的執行個體是以不值得在局部範圍使用它,如果為每個線程提供一個自己獨有的變量拷貝,将大大提高效率。首先,通過複用減少了代價高昂的對象的建立個數。其次,你在沒有使用高代價的同步或者不變性的情況下獲得了線程安全。

你如何在 Java 中擷取線程堆棧?

kill -3 [java pid]不會在目前終端輸出,它會輸出到代碼執行的或指定的地方去。比如,kill -3tomcat pid, 輸出堆棧到 log 目錄下。Jstack [java pid]這個比較簡單,在目前終端顯示,也可以重定向到指定檔案中。-JvisualVM:Thread Dump不做說明,打開 JvisualVM 後,都是界面操作,過程還是很簡單的。

Java 中 ConcurrentHashMap 的并發度是什麼?

ConcurrentHashMap 把實際 map 劃分成若幹部分來實作它的可擴充性和線程安全。這種劃分是使用并發度獲得的,它是 ConcurrentHashMap 類構造函數的一個可選參數,預設值為 16,這樣在多線程情況下就能避免争用。在 JDK8 後,它摒棄了 Segment(鎖段)的概念,而是啟用了一種全新的方式實作,利用 CAS 算法。同時加入了更多的輔助變量來提高并發度,具體内容還是檢視源碼吧。

volatile 變量和 atomic 變量有什麼不同?

Volatile 變量可以確定先行關系,即寫操作會發生在後續的讀操作之前, 但它并不能保證原子性。例如用 volatile 修飾 count 變量那麼 count++ 操作就不是原子性的。而 AtomicInteger 類提供的 atomic 方法可以讓這種操作具有原子性如getAndIncrement()方法會原子性的進行增量操作把目前值加一,其它資料類型和引用變量也可以進行相似操作。

你對線程優先級的了解是什麼?

每一個線程都是有優先級的,一般來說,高優先級的線程在運作時會具有優先權,但這依賴于線程排程的實作,這個實作是和作業系統相關的(OS dependent)。我們可以定義線程的優先級,但是這并不能保證高優先級的線程會在低優先級的線程前執行。線程優先級是一個 int 變量(從 1-10),1 代表最低優先級,10 代表最高優先級。java 的線程優先級排程會委托給作業系統去處理,是以與具體的作業系統優先級有關,如非特别需要,一般無需設定線程優先級。

并發,又是并發

如何確定線程安全?

同步方法和同步塊,哪個是更好的選擇?

如何避免死鎖?

  • 互斥條件:一個資源每次隻能被一個程序使用。
  • 不剝奪條件:程序已獲得的資源,在末使用完之前,不能強行剝奪。
  • 循環等待條件:若幹程序之間形成一種頭尾相接的循環等待資源關系。

    避免死鎖最簡單的方法就是阻止循環等待條件,将系統中所有的資源設定标志位、排序,規定所有的程序申請資源必須以一定的順序(升序或降序)做操作來避免死鎖。