1. 程式人生 > 其它 >併發程式設計-基礎

併發程式設計-基礎

執行緒基礎

執行緒的三種建立方法

繼承Thread類.java不支援多繼承,子類不能繼承其他類

實現runnable介面:還可以繼承其他類

實現callable介面:有返回值


執行緒相關JVM語句

cmder 常用語句記錄:

軟體:cmder 就是個cmd,就是個命令列,不過她是支援複製貼上的

jcmd:

傳送診斷命令請求到正在執行的Java虛擬機器(JVM)。它必須和JVM執行在同一臺機器上,並且與啟動JVM使用者具有相同的組許可權。

jcmd 31275 Thread.print -l # 列印執行緒棧
jcmd 31275 VM.command_line # 列印啟動命令及引數
jcmd 31275 GC.heap_dump /data/31275.dump # dump heap
jcmd 31275 GC.class_histogram #檢視類的統計資訊
jcmd 31275 VM.system_properties #檢視系統屬性內容
jcmd 31275 VM.uptime #檢視虛擬機器啟動時間
jcmd 31275 PerfCounter.print #檢視效能統計

jps -lvm 檢視所有在本機上執行java的程序

常用解析:

jstack

jstack -l 9552

打印出所有的執行緒堆疊

一般第一個就是銷燬執行緒,沒有daemon 守護執行緒標記,是一個使用者執行緒,是等待所有使用者執行緒執行完畢,用來銷燬java虛擬機器的執行緒

join()

注意:

join方法為Thread類直接提供的方法,而wait和notify為Object類中的方法

等待執行緒執行終止之後,繼續執行,比如

Thread t1 = new Thread();
t1.start();
t1.join(1000);//毫秒時間
System.out.println("t1's state:"+t1.getState());

就是先將t1執行緒start,有join,就阻塞主執行緒

這裡是主執行緒阻塞1秒,過了這個時間就繼續執行後面的程式碼

join()和join(1000)的區別

thread.Join把指定的執行緒加入到當前執行緒,可以將兩個交替執行的執行緒合併為順序執行的執行緒。

比如線上程B中呼叫了執行緒A的Join()方法,直到執行緒A執行完畢後,才會繼續執行執行緒B。

t.join(); //呼叫join方法,等待執行緒t執行完畢
t.join(1000); //等待 t 執行緒,等待時間是1000毫秒。

原始碼解析:

/**
 * Waits at most {@code millis} milliseconds for this thread to
 * die. A timeout of {@code 0} means to wait forever.
 *
 * <p> This implementation uses a loop of {@code this.wait} calls
 * conditioned on {@code this.isAlive}. As a thread terminates the
 * {@code this.notifyAll} method is invoked. It is recommended that
 * applications not use {@code wait}, {@code notify}, or
 * {@code notifyAll} on {@code Thread} instances.
 *
 * @param  millis
 *         the time to wait in milliseconds
 *
 * @throws  IllegalArgumentException
 *          if the value of {@code millis} is negative
 *
 * @throws  InterruptedException
 *          if any thread has interrupted the current thread. The
 *          <i>interrupted status</i> of the current thread is
 *          cleared when this exception is thrown.
 */
public final synchronized void join(long millis)
throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;

    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }

    if (millis == 0) {
        while (isAlive()) {
            wait(0);
        }
    } else {
        while (isAlive()) {
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

方法解釋:

最多等待 ms 毫秒讓該執行緒死亡。 超時為 0 意味著永遠等待。 此實現使用以 this.isAlive 為條件的 this.wait 呼叫迴圈。 當執行緒終止時,呼叫 this.notifyAll 方法。 建議應用程式不要在 Thread 例項上使用 wait、notify 或 notifyAll。

底層呼叫isAlive()方法,

Tests if this thread is alive. A thread is alive if it has been started and has not yet died.
Returns:
true if this thread is alive; false otherwise.
測試此執行緒是否存活。 如果執行緒已啟動且尚未死亡,則該執行緒處於活動狀態。
返回:
如果此執行緒還活著,則為真; 否則為假。 

總結:也就是如果t.join()就和wait類似,主執行緒要等待這個t執行緒結束才能執行下去,底層呼叫object的wait()方法

join(xxx)表示最多等待xxx毫秒,如果在這個時間內執行緒執行完畢,則立即執行join後面的程式碼。


sleep()

不釋放鎖——原子性

Thread類中的一個靜態方法,暫時讓出執行權,不參與CPU排程,但是。時間到了就進入到就緒狀態,一旦獲取到CPU時間片,則繼續執行。

是一個native static方法

可以用中斷異常

t1.interrupt();


yield()—假裝客氣

不釋放鎖——原子性

知識點:Thread類中的靜態native方法;讓出剩餘的時間片,本身進入就緒狀態,CPU再次排程還可能排程到本執行緒。

易混淆知識點:sleep是在一段時間內進入阻塞狀態,cpu不會排程它。而yield是讓出執行權,本身還處於就緒狀態,cpu還可能立即排程它,可以不理會它


wait()/notify-object

釋放鎖

notify不釋放鎖

有且僅有wait會釋放鎖

wait一般與synchronize+notify連用,因為會釋放鎖


interrupt()

執行緒中斷

知識點:執行緒中斷是執行緒間的一種協作模式,通過設定執行緒中斷標誌來實現,執行緒根據這個標誌來自行處理。

public void interrupt() 中斷標誌設定為true

public boolean isInterrupted() 獲取中斷標記

public static boolean interrupted() 將中斷標記取反,會改變當前執行緒的中斷標記狀態

​ 獲取當前執行緒的狀態:容易混淆!,在主函式中就是獲取main執行緒

會導致sleep和join執行緒丟擲中斷異常


執行緒安全問題原理

可見性和原子性

1.執行緒安全的概念:當多個執行緒訪問某一個類、物件或方法時,這個類、物件或方法都能表現出與單

執行緒執行時一致的行為,那麼這個類、物件或方法就是執行緒安全的。

2.執行緒安全問題都是由全域性變數及靜態變數引起的。

3.若每個執行緒中對全域性變數、靜態變數只有讀操作,而無寫操作,一般來說,這個全域性變數是執行緒

安全的;若有多個執行緒同時執行寫操作,一般都需要考慮執行緒同步,否則的話就可能影響執行緒安全。


synchronized關鍵字

原理就是保證原子性,維護可見性,維護秩序。

Synchronized的作用是加鎖,所有的synchronized方法都會順序執行,(這裡只佔用CPU的順序)。

Synchronized方法執行方式:

– 首先嚐試獲得鎖

– 如果獲得鎖,則執行Synchronized的方法體內容。

– 如果無法獲得鎖則等待,並且不斷的嘗試去獲得鎖,一旦鎖被釋放,則多個執行緒會同時去嘗

試獲得所,造成鎖競爭問題。

鎖競爭問題,在高併發、執行緒數量高時會引起CPU佔用居高不下,或者直接宕機。


物件鎖和類鎖--區別

Synchronized作用在非靜態方法上代表的物件鎖,一個物件一個鎖,多個物件之間不會發生鎖競爭。

Synchronized作用在靜態方法上則升級為類鎖,所有物件共享一把鎖,存在鎖競爭。

比如

User user1 = new User();
User user2 = new User();
synchronized(use1)
synchronized(use2)
鎖是不同的物件,不同物件之間吧劊發生鎖競爭

但是如果   static synchronize  是類鎖的話就會發生競爭 

原理是類鎖的話,靜態變數和靜態方法是儲存在方法區中的,訪問的是同一個資料

但是物件鎖而言,物件是儲存在堆中的,比如上述的user1和user2都在堆中是不同的物件,不是同一個資源,所以不存在鎖競爭

一個類裡面如果方法上有synchronize表示當前的鎖是當前類物件


物件鎖的同步和非同步

同步:必須等待方法執行完畢,才能向下執行,共享資源訪問的時候,為了保證執行緒安全,必須同步。

非同步:不用等待其他方法執行完畢,即可立即執行,例如Ajax非同步。

物件鎖只針對synchronized修飾的方法生效、 物件中的所有synchronized方法都會同步執行、而非

synchronized方法非同步執行

避免誤區:類中有兩個synchronized方法,兩個執行緒分別呼叫兩個方法,相互之間也需要競爭鎖,

因為兩個方法從屬於一個物件,而我們是在物件上加鎖

髒讀問題

多個執行緒訪問同一個資源,在一個執行緒修改資料的過程中,有另外的執行緒來讀取資料,就會引起髒

讀的產生。

為了避免髒讀我們一定要保證資料修改操作的原子性、並且對讀取操作也要進行同步控制

鎖重入

同一個執行緒得到了一個物件的鎖之後,再次請求此物件時可以再次獲得該物件的鎖。

父子類可以鎖重入

public class DemoThread05{
   
   public synchronized void run1(){
      System.out.println(Thread.currentThread().getName()+">run1...");
      //呼叫同類中的synchronized方法不會引起死鎖
      run2();
   }
   
   public synchronized void run2(){
      System.out.println(Thread.currentThread().getName()+">run2...");
   }
   
   public static void main(String[] args) {
      final DemoThread05 demoThread05 = new DemoThread05();
      Thread thread = new Thread(new Runnable() {
         @Override
         public void run() {
            demoThread05.run1();
         }
      });
      thread.start();
   }
}

拋異常和鎖的關係

一個執行緒在獲得鎖之後執行操作,發生錯誤丟擲異常,則自動釋放鎖

1、可以利用丟擲異常,主動釋放鎖

2、程式異常時防止資源被死鎖、無法釋放

3、異常釋放鎖可能導致資料不一致

synchronized程式碼塊

可以達到更細粒度的控制

當前物件鎖

類鎖

任意物件鎖

總結:

​ 同類型鎖之間互斥,不同型別的鎖之間互不干擾

不要線上程內修改物件鎖的引用

不能修改鎖的指向地址。如果是物件,修改物件裡的值是不會引起鎖失效的

併發和死鎖

是指兩個或兩個以上的程序在執行過程中,由於競爭資源或者由於彼此通訊而造成的一種阻塞的現

象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖,這些永

遠在互相等待的程序稱為死鎖程序。

執行緒間的通訊

每個執行緒都是獨立執行的個體,執行緒通訊能讓多個執行緒之間協同工作

Object類中的wait/notify方法可以 實現執行緒間通訊

Wait/notify必須與synchronized一同使用

Wait釋放鎖、notify不釋放鎖

notify與notifyAll的區別

notify本身不釋放鎖

Notify只會通知一個wait中的執行緒,並把鎖給他,不會產生鎖競爭問題,但是該執行緒處理完畢之後

必須再次notify或notifyAll,完成類似鏈式的操作。

NotifyAll會通知所有wait中的執行緒,會產生鎖競爭問題

notify的通知依賴底層JVM的實現,這裡notify依賴的是native方法,找到原始碼,目前是先進先出,就是最早wait()的先被喚醒。

synchronized的wait和notify是位於ObjectMonitor.cpp中

這裡實際上是將_WaitSet中的第一個元素進行出隊操作

原來hotspot對notofy()的實現並不是我們以為的隨機喚醒, 而是“先進先出”的順序喚醒!`

實戰:實現阻塞式執行緒安全佇列

使用synchronized、wait、notify實現帶阻塞的執行緒安全佇列

功能描述:

功能1:在佇列元素滿的時候,put阻塞,在佇列元素空的時候,get阻塞

功能2 :執行緒安全

程式碼:DemoThread20

視訊中的程式碼 仍然有bug,不過我已經改正了

while (this.list.size() == this.maxSize) {
    lock.wait();
}

問題就是wait中的執行緒被喚醒時和新執行緒競爭的時候破壞了資料的原子性,因為集合是非原子性的

守護執行緒和使用者執行緒

執行緒分類:daemon執行緒(守護執行緒)、user執行緒(使用者執行緒)

易混淆知識點:main函式所在的執行緒就是一個使用者執行緒

重要知識點1:最後一個user執行緒結束時,JVM會正常退出,不管是否有守護執行緒正在執行。反過來說:只要有一個使用者執行緒還沒結束,JVM程序就不會結束。

重要知識點2:父執行緒結束後,子執行緒還可以繼續存活,子執行緒的生命週期不受父執行緒的影響

shutdown和kill的區別

shutdown會將所有正在執行的請求執行完畢,而kill不會,尤其是kill -9

執行緒上下文切換

CPU採用時間片輪巡的策略,執行緒切換的過程就是上下文切換

當前執行緒使用完時間片後就會進入就緒狀態,讓出CPU執行權給其他執行緒,此時就是從當前執行緒的上下文切換到了其他執行緒。

當發生上下文切換的時候需要儲存執行現場,待下次執行時進行恢復。

所以頻繁的、大量的上下文切換會造成一定資源開銷

驗證

shutdown會將所有controller正在執行的請求執行完畢,而kill不會