1. 程式人生 > 實用技巧 >多執行緒基礎概念總結

多執行緒基礎概念總結

-----文主要記錄多執行緒一些基本知識與概念,算是一個小總結。關於同步器,執行緒池那些並沒有涉及。

單執行緒下的程式,是一段程式碼一段程式碼按順序執行,而一些程式碼在執行時往往用不到 CPU,這樣CPU就得不到充分的利用,效率非常低下。所以 "併發程式設計" 這一概念就產生了,我們可以在沒有用到 CPU的時候去執行其他程式碼來提高總體程式碼的執行效率。比如在修改檔案時,CPU 需要先等待讀取檔案,然後才能進行修改。在讀取過程中 CPU就處於空閒狀態,這時我們可以讓他去執行其他程式碼來減少總程式碼的執行時間。

基本概念

併發多個任務在同一個 CPU核上,按細分的時間塊交替執行

並行多個處理器或多核處理器同時處理多個任務,是真正意義上的同時執行。

序列多個任務按順序執行,一個任務執行完後再開始執行下一個任務。

同步指的是能按預期的方式去進行,也就是能 "控制"的執行。

併發程式設計多個執行緒以併發的方式執行

程序一段程式的執行過程。是作業系統進行資源分配的最小單位。

執行緒程序執行的單位,是處理器任務排程和執行的最小單位。

程序與執行緒的區別

1、每個程序都有獨立的程式碼的資料空間(程式上下文),程式之間的切換會有較大的開銷;執行緒可以看作是輕量級的程序,同一類執行緒共享程式碼和資料空間,每個執行緒都有自己獨立的執行棧和執行緒計數器,執行緒之間切換的開銷小。

2、程序是包含執行緒的,一個程序包含若干個執行緒,一個程序內的執行緒共享這個程序的空間和資源。而程序之間的地址空間和資源是互相獨立。

3、一個程序崩潰後,不會對其他程序造成影響;一個執行緒崩潰後,包含這個執行緒的程序都會收到影響。

4、每個獨立的程序包含程式執行的入口、順序執行序列和程式出口;執行緒不能獨立執行,必須依附於程序才能執行。

併發程式設計的三要素原子性(不會在執行過程中受到其他執行緒的影響)、可見性(每次獲取的都是記憶體中最新的值)、有序性(禁止指令重排)。

JMM

  在正式瞭解併發程式設計之前,要先理解 java記憶體模型( java Memory Model),主要規定了在併發程式設計中變數儲存的規則。主變數是儲存在主記憶體中的,而每個執行緒在進行變數的計算時是先將變數拷貝一個副本到當前執行緒的本地記憶體中去的,然後在本地記憶體進行修改,然後再更新到主記憶體中去。這就是執行緒修改資料的過程。


多執行緒基礎

  執行緒狀態

  執行緒主要有五種狀態,新建、就緒、執行、阻塞、死亡。

當一個執行緒物件建立完成後就進入 "新建"狀態,隨後呼叫 start()方法後就會等待 CPU排程分配,此時就是 "就緒"狀態,等到獲得了 CPU排程後,就開始執行執行緒程式碼,此時就是 "執行"狀態。如果在程式碼中呼叫了 join()、sleep()、wait()方法,當前執行緒就會阻塞,進入 "阻塞"狀態,join()是讓呼叫此方法的執行緒先執行完再繼續執行當前執行緒,所以在呼叫此方法後當前執行緒就會進入 "阻塞"狀態,直到呼叫 join() 方法的執行緒執行完畢才會先進入 "就緒"狀態,然後才恢復到 "執行"狀態;sleep()方法則是一個靜態方法 ,同時也是一個本地方法,無論哪個執行緒物件呼叫 sleep()方法,都會使當前執行緒進入 "阻塞"狀態,直到規定的時間到達後,才會先進入 "就緒"狀態,然後才會再進入 "執行"狀態;wait()方法同理,只不過它是直到呼叫 notify()才會進入 "就緒"狀態,然後去等待 CPU排程。當執行緒程式碼執行完畢,就會中斷當前執行緒,釋放 CPU資源,然後死亡。

  執行緒建立

  建立執行緒主要有三種方式:1、繼承 Thread  2、實現 Runnable介面  3、實現 Callable介面

  1、繼承 Thread

 1 public class StartThread extends Thread{
 2     @Override
 3     public void run() {
 4         for(int i=0;i<20;i++) {
 5             System.out.println("程序"+currentThread().getName());
 6         }
 7     }
 8 
 9     public static void main(String[] args) {
10         StartThread st=new StartThread();
11         st.start();                               
12         for(int i=0;i<20;i++) {
13             System.out.println("1111111");
14         }
15 
16     }
17 
18 }

  thread類是執行緒類,它的 run方法就對應著執行緒執行的內容,我們在建立繼承 Thread類的類的物件時,只需要呼叫 start() 方法就可以喚醒這個執行緒,讓他啟動起來,需要注意的是,如果直接呼叫 run() 方法的話,是不能達成多執行緒的執行的,它相當於只是呼叫這個方法 "阻塞式"的執行。這種方式也是比較常見的建立多執行緒的方式,但是因為 Java中的類是單繼承的,所以通過這種方式來建立執行緒就不能再去繼承其他類了,所以這種方式並不是最佳方案。

多執行緒下不能同步:值得注意的是,主執行緒 main和新建的執行緒 st並不是按順序執行的,多執行幾次就會產生類似下面這種結果:

不同執行緒之間是交替執行的,這就是多執行緒 "併發" 的特點,這個交替的順序是不能人為控制的。這就是執行緒的不能 "同步" 性。

  2、實現 Runnable介面

 1 public class StartRun implements Runnable{
 2     public void run() {
 3         for(int i=0;i<10;i++) {
 4             System.out.println(i + "_________________");
 5         }
 6     }
 7 
 8     public static void main(String[] args) {
 9         new Thread(new StartRun()).start();;
10         for(int i=0;i<10;i++) {
11             System.out.println(i);
12         }
13 
14     }
15 
16 }

  相比於繼承 Thread類,實現 Runnable類更加靈活,因為一個類是可以實現多個介面的,所以我們在實現 Runnable並不影響我們的繼承和實現。這也是最常用的建立執行緒的方式。同時 Thread類也是 Runnable介面的實現類,實現了 Runnable介面的 run方法,這也能說明為什麼這兩種方式都需要去重寫和實現 run方法。

  3、實現 Callable介面

 1 public class FutureTaskTest implements Callable<String>{
 2     @Override
 3     public String call() throws Exception {
 4         Thread.sleep(2000);
 5         System.out.println("123");
 6         return 12+"";
 7     }
 8     public static void main(String[] args) throws InterruptedException, ExecutionException {
 9         FutureTask<String> future=new FutureTask<String>(new FutureTaskTest());
10         new Thread(future).start();
11          System.out.println("獲取值");
12          System.out.println(future.isCancelled());
13          System.out.println(future.isDone());
14          System.out.println(future.get());        
15     }
16 }

這種方式是比較特殊的一種,因為上面兩種執行緒在執行時執行的是 run方法,而 run方法是沒有返回值的,而使用這種方式來建立執行緒實現的是 call()方法,並且 call()是有返回值的,返回值型別是和實現介面指定的泛型一致。這就意味著我們可以呼叫某個方法去獲取這個返回值,下面就詳細說一下 Callable介面相關的方法。

@FunctionalInterface
public interface Callable<V> {
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    V call() throws Exception;
}

上面是 Callable介面的原始碼,可以看到裡面只有一個 call()方法,和 Runnable介面如出一轍,我們知道建立一個執行緒物件就是建立一個 Thread物件,但是在檢視 Thread類的原始碼後,會發現沒有 Callable型別的建構函式

那麼我們該如何建立 Callable型別的執行緒呢?這就要說到 FutureTask類來間接建立 Thread物件了,還是先看一下 FutureTask的原始碼

public class FutureTask<V> implements RunnableFuture<V> {

    /**
     * Creates a {@code FutureTask} that will, upon running, execute the
     * given {@code Callable}.
     *
     * @param  callable the callable task
     * @throws NullPointerException if the callable is null
     */
 public FutureTask(Callable<V> callable) {
        if (callable == null)
            throw new NullPointerException();
        this.callable = callable;
        this.state = NEW;       // ensure visibility of callable
    }

    /**
     * Creates a {@code FutureTask} that will, upon running, execute the
     * given {@code Runnable}, and arrange that {@code get} will return the
     * given result on successful completion.
     *
     * @param runnable the runnable task
     * @param result the result to return on successful completion. If
     * you don't need a particular result, consider using
     * constructions of the form:
     * {@code Future<?> f = new FutureTask<Void>(runnable, null)}
     * @throws NullPointerException if the runnable is null
     */
    public FutureTask(Runnable runnable, V result) {
        this.callable = Executors.callable(runnable, result);
        this.state = NEW;       // ensure visibility of callable
    }

}

由於篇幅只截了構造器和類部分。首先可以看到,FutureTask內部有 Callable型別引數的構造器,所以可以通過它來建立 Callable型別物件的物件,再看它的結構,FutureTask實現了 RunnableFuture介面,那麼這個介面的結構是什麼呢?

1 public interface RunnableFuture<V> extends Runnable, Future<V> {
2     /**
3      * Sets this Future to the result of its computation
4      * unless it has been cancelled.
5      */
6     void run();
7 }

這個介面繼承了 Runnable介面,所以我們可以將 FutureTask物件作為引數來建立 Thread物件。同時從開始的例子可以看到這個類也有一些特殊的方法。下面將一一講解。

1、get()

這個方法是用來獲取執行緒執行方法 call()方法的返回值的,如果該執行緒程式碼還沒有執行完,會一直等待,直到執行完得到返回值。

2、cancel(boolean mayInterruptIfRunning)

取消 Callable 執行緒 call()方法的執行,如果 call()方法已經執行結束,或者已經被取消,或者不能被取消,這個方法就會執行失敗並返回false;如果call()方法還沒有開始執行,那麼call()方法會被取消,不會再被執行;如果call()方法已經開始執行了,但是還沒有執行結束,這時如果呼叫 get() 方法會丟擲異常,至於過程會不會執行會根據 mayInterruptIfRunning 的值,如果mayInterruptIfRunning = true,那麼會中斷call()方法的執行緒,然後返回true,如果引數為false,會返回true,不會中斷call()方法的執行緒。需要注意,這個方法執行結束,返回結果之後,再呼叫isDone()會返回true

3、isCancelled()

返回 cancel方法的結果,只要沒有成功中斷 call()方法都是 true。

4、isDone()

是否執行結束,如果執行完畢返回 true,負責返回 false。

現在再來看上面的例子,首先會建立FutureTask物件,再建立對應的執行緒物件並呼叫 start()方法,執行緒開始進入 "就緒"狀態,隨後開始與主執行緒 "併發"執行,而 future 執行緒呼叫 Sleep()方法,所以輸出臺還是先執行主執行緒的輸出語句,等執行到 get() 方法就會阻塞當前執行緒,並且主執行緒前面的輸出程式碼執行消耗時間比較短,所以約2秒後,future sleep()方法結束,繼續執行,等到 future 執行緒執行完,那麼 get()方法得到返回值,輸出返回值 "12" 。

而如果在main函式中呼叫完 future執行緒的 start()方法後立刻呼叫 future.cancel(true),那麼 future執行緒的執行就會取消,同時執行到 get() 方法時會丟擲異常。

  跳出阻塞

  一般情況下當一個執行緒進入 "阻塞"狀態後就不能繼續執行程式碼,必須達到必要的條件後才能繼續執行程式碼,比如呼叫 Sleep()方法後就需要等到規定的時間結束後才能退出 "阻塞"狀態,那麼有沒有別的方法提前退出 "阻塞"狀態呢?答案是有的。事實上,每個執行緒都有一個 "中斷狀態",當我們在呼叫 sleep() ,join(),wait()方法時都需要處理InterruptedException異常,這個異常就是中斷異常,當該執行緒中斷狀態為 true後進入 "阻塞"狀態後就會丟擲 InterruptedException異常,然後中斷 "阻塞" 狀態,跳出try catch包圍的程式碼繼續執行,如果是方法上的丟擲異常那麼就跳到上一級方法。下面先介紹中斷的幾個方法。

1、interrupt()

呼叫此方法的執行緒會將中斷狀態設為 true,也就是開啟中斷狀態(執行緒預設中斷狀態是 false)。

2、interrupted()

是一個靜態方法,無論哪個執行緒物件呼叫此方法,它的實現都是返回當前執行緒的中斷狀態,並且如果當前執行緒中斷狀態是 true,再設為 false。

3、isInterrupted()

返回呼叫次方法的執行緒的中斷狀態。

下面來看一個例子。

 1 public class InterruptionInJava implements Runnable{
 2 
 3     public static void main(String[] args) throws InterruptedException {
 4 
 5         Thread testThread = new Thread(new InterruptionInJava(),"InterruptionInJava");
 6 
 7         testThread.start();
 8         Thread.sleep(3000);
 9 
10         System.out.println("中斷前中斷狀態:" + testThread.isInterrupted());
11         testThread.interrupt();
12 
13         System.out.println("中斷後中斷狀態:" + testThread.isInterrupted());
14 
15     }
16 
17     @Override
18     public void run() {
19         try {
20             System.out.println("testThread 開始執行");
21             Thread.sleep(10000000);
22         } catch (InterruptedException e) {
23             // TODO Auto-generated catch block
24             e.printStackTrace();
25         }
26         System.out.println("testThread 執行結束");
27     }
28 }

執行結果:

在testThread執行緒啟動後,主執行緒呼叫 sleep()進入短暫的 "阻塞" 狀態,而testThread在輸出通知語句後進入長時間的 "阻塞",等到主執行緒的 sleep()時間到時,呼叫testThread的 interrupt()方法,讓testThread 的中斷狀態開啟,這時testThread就會跳出 "阻塞"狀態,然後繼續執行 try catch外面的程式碼,而主執行緒在呼叫testThread的 interrupt()方法前後輸出的中斷狀態不同。

執行緒同步

  上面已經說過了,多執行緒的執行是 "併發"的,所以當多個執行緒需要對同一個資料進行不同的操作時,往往會因為執行順序的不同而造成不同的結果。這就是執行緒沒有同步造成了資料的不安全性。而保證執行緒同步就成為了多執行緒程式設計最重要的一個環節。保證執行緒同步的方式可以是直接使用 synchronized、lock鎖鎖住特定的物件,讓同一時間內只有一個執行緒去執行;也可以使用一些同步容器,譬如 ConcurrentHashMap、Vector、ThreadLocal等;還有一些是控制併發量的,比如 Semaphore、CycliBarriar、CountdownLatch等。其中 synchronized和 lock已經另開文章說明了,同步容器 ConcurrentHashMap也比較詳細的分析過,併發量相關的暫時還沒有過分去接觸,後面有時間補習一下再記錄。下面就簡單說一下AQS。

  AQS

  AQS可以說是JUC的基石,許多同步器都是通過AQS實現的,它規定了多執行緒情況下各個執行緒對資源的爭奪規則。

  AQS內部維護了一個int型別的屬性State,這個屬性用來表示當前資源的佔用情況,如果是0表示資源是空閒的,如果>0表示資源被佔用,當資源被一個執行緒獲取並加鎖後,state就會+1,當其他執行緒嘗試獲取資源時就會來檢視這個state,如果>0,就放棄爭奪,同時如果一個執行緒在已擁有鎖的情況下再次對這個物件進行加鎖操作(前提是可重入鎖),那麼state就會再次+1,後面每釋放一次state會-1,直到變成0。

  除此之外,AQS內部還維護了一個FIFO佇列,它是由一個雙向連結串列組成的,當執行緒獲取資源失敗時,就會進入這個佇列阻塞並等待CPU排程分配資源。

  AQS正是通過這個這兩個機制才能保證資源的有序分配,保證同步。