併發_01_併發性與多線程介紹

一、併發與多線程

1.1 什麼是進程、線程?線程和進程的區別?

進程
當一個程序進入內存運行時,即變成一個進程。進程是處於運行過程中的程序。

Java VM 啓動的時候會有一個進程java.exe.

  • 該進程中至少一個線程負責java程序的執行。 而且這個線程運行的代碼存在於main方法中。該線程稱之爲主線程。該線程稱之爲主線程。
  • 其實更細節說明jvm,jvm啓動不止一個線程,還有負責垃圾回收機制的線程。

進程的三個特徵:

  • 獨立性
    獨立存在的實體,每個進程都有自己獨立私有的一塊內存空間。

  • 動態性
    程序只是一個靜態的指令集合,而進程是一個正在系統中活動的指令集合。

  • 併發性
    多個進程可在單處理器上併發執行。

併發性和並行性

  • 併發
    是指在同一時間點只能有一條指令執行,但多個進程指令被快速輪換執行,使得在宏觀上具有多個進程同時執行的效果。
  • 並行
    指在同一時間點,有多條指令在多個處理器上同時執行。

線程

  • 線程是操作系統能夠進行運算調度的最小單位,它被包含在進程之中,是進程中的實際運作單位。
  • 線程也被稱作輕量級進程。線程在進程中是獨立,併發的執行流。

線程和進程的區別

  • 線程是進程的組成部分,一個進程可以有很多線程,每條線程並行執行不同的任務。

  • 不同的進程使用不同的內存空間,而線程與父進程的其他線程共享父進程的所擁有的全部資源。
    別把內存空間和棧內存搞混,每個線程都擁有單獨的棧內存用來存儲本地數據。線程擁有自己的堆棧、自己的程序計數器和自己的局部變量,但不擁有系統資源。

  • 線程的調度和管理由進程本身負責完成。操作系統對進程進行調度,管理和資源分配。

1.2多線程的優點

Java 是最先支持多線程的開發的語言之一,Java 從一開始就支持了多線程能力

1.2.1 資源利用率更好

想象一下,一個應用程序需要從本地文件系統中讀取和處理文件的情景。比方說,從磁盤讀取一個文件需要 5 秒,處理一個文件需要 2 秒。處理兩個文件則需要:

5秒讀取文件A
2秒處理文件A
5秒讀取文件B
2秒處理文件B
---------------------
總共需要14秒

從磁盤中讀取文件的時候,大部分的 CPU 時間用於等待磁盤去讀取數據。在這段時間裏,CPU 非常的空閒。它可以做一些別的事情。通過改變操作的順序,就能夠更好的使用 CPU 資源。看下面的順序:

5秒讀取文件A
5秒讀取文件B + 2秒處理文件A
2秒處理文件B
---------------------
總共需要12秒

CPU 等待第一個文件被讀取完。然後開始讀取第二個文件。當第二文件在被讀取的時候,CPU 會去處理第一個文件。記住,在等待磁盤讀取文件的時候,CPU大 部分時間是空閒的。

總的說來,CPU 能夠在等待 IO 的時候做一些其他的事情。這個不一定就是磁盤 IO。它也可以是網絡的 IO,或者用戶輸入。通常情況下,網絡和磁盤的 IO 比 CPU 和內存的 IO 慢的多。

1.2.2 程序設計更簡單

在單線程應用程序中,如果你想編寫程序手動處理上面所提到的讀取和處理的順序,你必須記錄每個文件讀取和處理的狀態。相反,你可以啓動兩個線程,每個線程處理一個文件的讀取和操作。線程會在等待磁盤讀取文件的過程中被阻塞。在等待的時候,其他的線程能夠使用 CPU 去處理已經讀取完的文件。其結果就是,磁盤總是在繁忙地讀取不同的文件到內存中。這會帶來磁盤和 CPU 利用率的提升。而且每個線程只需要記錄一個文件,因此這種方式也很容易編程實現。

1.2.3 程序響應更快

將一個單線程應用程序變成多線程應用程序的另一個常見的目的是實現一個響應更快的應用程序。設想一個服務器應用,它在某一個端口監聽進來的請求。當一個請求到來時,它去處理這個請求,然後再返回去監聽。
服務器的流程如下所述:

while(server is active){
    listen for request
    process request
}

如果一個請求需要佔用大量的時間來處理,在這段時間內新的客戶端就無法發送請求給服務端。只有服務器在監聽的時候,請求才能被接收。另一種設計是,監聽線程把請求傳遞給工作者線程(worker thread),然後立刻返回去監聽。而工作者線程則能夠處理這個請求併發送一個回覆給客戶端。這種設計如下所述:

while(server is active){
    listen for request
    hand request to worker thread
}

這種方式,服務端線程迅速地返回去監聽。因此,更多的客戶端能夠發送請求給服務端。這個服務也變得響應更快。

桌面應用也是同樣如此。如果你點擊一個按鈕開始運行一個耗時的任務,這個線程既要執行任務又要更新窗口和按鈕,那麼在任務執行的過程中,這個應用程序看起來好像沒有反應一樣。相反,任務可以傳遞給工作者線程(word thread)。當工作者線程在繁忙地處理任務的時候,窗口線程可以自由地響應其他用戶的請求。當工作者線程完成任務的時候,它發送信號給窗口線程。窗口線程便可以更新應用程序窗口,並顯示任務的結果。對用戶而言,這種具有工作者線程設計的程序顯得響應速度更快。

1.3 多線程的代價

從一個單線程的應用到一個多線程的應用並不僅僅帶來好處,它也會有一些代價。不要僅僅爲了使用多線程而使用多線程。而應該明確在使用多線程時能多來的好處比所付出的代價大的時候,才使用多線程。如果存在疑問,應該嘗試測量一下應用程序的性能和響應能力,而不只是猜測。

1.3.1 設計更復雜

雖然有一些多線程應用程序比單線程的應用程序要簡單,但其他的一般都更復雜。在多線程訪問共享數據的時候,這部分代碼需要特別的注意。線程之間的交互往往非常複雜。不正確的線程同步產生的錯誤非常難以被發現,並且重現以修復。

1.3.2 上下文切換的開銷

當 CPU 從執行一個線程切換到執行另外一個線程的時候,它需要先存儲當前線程的本地的數據,程序指針等,然後載入另一個線程的本地數據,程序指針等,最後纔開始執行。這種切換稱爲“上下文切換”(“context switch”)。CPU 會在一個上下文中執行一個線程,然後切換到另外一個上下文中執行另外一個線程。

上下文切換並不廉價。如果沒有必要,應該減少上下文切換的發生。

你可以通過維基百科閱讀更多的關於上下文切換相關的內容:

http://en.wikipedia.org/wiki/Context_switch

1.3.3 增加資源消耗

線程在運行的時候需要從計算機裏面得到一些資源。除了CPU,線程還需要一些內存來維持它本地的堆棧。它也需要佔用操作系統中一些資源來管理線程。我們可以嘗試編寫一個程序,讓它創建 100 個線程,這些線程什麼事情都不做,只是在等待,然後看看這個程序在運行的時候佔用了多少內存。

二 、多線程的實現方法

Java 中實現多線程有兩種方法:繼承 Thread 類、實現 Runnable 接口,在程序開發中只要是多線程,肯定永遠以實現 Runnable 接口爲主,因爲實現 Runnable 接口相比繼承 Thread 類有如下優勢:

  • 可以避免由於 Java 的單繼承特性而帶來的侷限;
  • 增強程序的健壯性,代碼能夠被多個線程共享,代碼與數據是獨立的;
  • 適合多個相同程序代碼的線程區處理同一資源的情況。
    下面以典型的買票程序(基本都是以這個爲例子)爲例,來說明二者的區別。

2.1 首先通過繼承 Thread 類實現

  • 代碼如下:
class MyThread extends Thread {
    private int ticket = 5;

    public void run() {
        for (int i = 0; i < 10; i++) {
            synchronized (this) {
                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName() + ":ticket = " + ticket--);
                }
            }

        }
    }
}

public class ThreadDemo {
    public static void main(String[] args) {
        new MyThread().start();
        new MyThread().start();
        new MyThread().start();
    }
}
  • 運行結果:
Thread-0:ticket = 5
Thread-0:ticket = 4
Thread-0:ticket = 3
Thread-0:ticket = 2
Thread-0:ticket = 1
Thread-1:ticket = 5
Thread-1:ticket = 4
Thread-1:ticket = 3
Thread-1:ticket = 2
Thread-1:ticket = 1
Thread-2:ticket = 5
Thread-2:ticket = 4
Thread-2:ticket = 3
Thread-2:ticket = 2
Thread-2:ticket = 1

從結果中可以看出,每個線程單獨賣了 5 張票,即獨立地完成了買票的任務,但實際應用中,比如火車站售票,需要多個線程去共同完成任務,在本例中,即多個線程共同買 5 張票。

2.2 下面是通過實現 Runnable 接口實現的多線程程序

  • 代碼如下:
class MyThread implements Runnable {
    public int ticket = 5;

    public void run() {
        for (int i = 0; i < 10; i++) {
            synchronized (this) {
                if (ticket > 0) {
                    System.out.println(Thread.currentThread().getName() + ":ticket = " + ticket--);
                }
            }

        }
    }
}

public class RunnableDemo {
    public static void main(String[] args) {
        MyThread my = new MyThread();
        new Thread(my).start();
        new Thread(my).start();
        new Thread(my).start();
    }
}
  • 運行結果
Thread-0:ticket = 5
Thread-0:ticket = 4
Thread-0:ticket = 3
Thread-1:ticket = 2
Thread-1:ticket = 1

從結果中可以看出,三個線程一共賣了 5 張票,即它們共同完成了買票的任務,實現了資源的共享。

thread 類中的start() 和 run() 方法有什麼區別
start()方法被用來啓動新創建的線程,而且start()內部調用了run()方法,這和直接調用run()方法的效果不一樣。
當你調用run()方法的時候,只會是在原來的線程中調用,沒有新的線程啓動,start()方法纔會啓動新線程。 需要特別注意的是:不能對同一線程對象兩次調用start()方法。

2.3 線程的生命週期

Java線程五種狀態:

  • 新建狀態(New)
    當線程對象創建後,即進入了新建狀態。僅僅由java虛擬機分配內存,並初始化。如:Thread t = new MyThread();

  • 就緒狀態(Runnable)
    當調用線程對象的start()方法(t.start();),線程即進入就緒狀態。處於就緒狀態的線程,java虛擬機創建方法調用棧和程序計數器,只是說明此線程已經做好了準備,隨時等待CPU調度執行,此線程並 沒有執行。

  • 運行狀態(Running)
    當CPU開始調度處於就緒狀態的線程時,執行run()方法,此時線程才得以真正執行,即進入到運行狀態。注:緒狀態是進入到運行狀態的唯一入口,也就是說,線程要想進入運行狀態執行,首先必須處於就緒狀態中;

  • 阻塞狀態(Blocked)
    處於運行狀態中的線程由於某種原因,暫時放棄對CPU的使用權,停止執行,此時進入阻塞狀態,直到其進入到就緒狀態,才 有機會再次被CPU調用以進入到運行狀態。根據阻塞產生的原因不同,阻塞狀態又可以分爲三種:

  • 等待阻塞
    運行狀態中的線程執行wait()方法,使本線程進入到等待阻塞狀態,JVM會把該線程放入等待池中;

  • 同步阻塞
    線程在獲取synchronized同步鎖失敗(因爲鎖被其它線程所佔用),它會進入同步阻塞狀態;

  • 其他阻塞
    通過調用線程的sleep()或join()或發出了I/O請求時,線程會進入到阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者I/O處理完畢時,線程重新轉入就緒狀態。

  • 死亡狀態(Dead)
    線程run()方法執行完了或者因異常退出了run()方法,該線程結束生命週期。 當主線程結束時,其他線程不受任何影響。

2.4 java控制線程方法

2.4.1 join線程

join方法用線程對象調用,如果在一個線程A中調用另一個線程B的join方法,線程A將會等待線程B執行完畢後再執行。

2.4.2 守護線程(Daemon Thread)

Java中有兩類線程

  • User Thread(用戶線程)、Daemon Thread(守護線程)

    用戶線程即運行在前臺的線程,而守護線程是運行在後臺的線程。
    守護線程作用是爲其他前臺線程的運行提供便利服務,而且僅在普通、非守護線程仍然運行時才需要,比如垃圾回收線程就是一個守護線程。當VM檢測僅剩一個守護線程,而用戶線程都已經退出運行時,VM就會退出,因爲沒有如果沒有了被守護這,也就沒有繼續運行程序的必要了。如果有非守護線程仍然存活,VM就不會退出。 守護線程的特徵:如果所有前臺線程都死亡,後臺線程會自動死亡。 守護線程並非只有虛擬機內部提供,用戶在編寫程序時也可以自己設置守護線程。用戶可以用Thread的setDaemon(true)方法設置當前線程爲守護線程。 雖然守護線程可能非常有用,但必須小心確保其他所有非守護線程消亡時,不會由於它的終止而產生任何危害。因爲你不可能知道在所有的用戶線程退出運行前,守護線程是否已經完成了預期的服務任務。一旦所有的用戶線程退出了,虛擬機也就退出運行了。 因此,不要在守護線程中執行業務邏輯操作(比如對數據的讀寫等)。

    另外有幾點需要注意:

    • 1、setDaemon(true)必須在調用線程的start()方法之前設置,否則會跑出IllegalThreadStateException異常。
    • 2、在守護線程中產生的新線程也是守護線程。
    • 3、 不要認爲所有的應用都可以分配給守護線程來進行服務,比如讀寫操作或者計算邏輯。

2.4.3 線程讓步(yield )

  • yield可以直接用Thread類調用,可以讓當前正在執行的線程暫停,不會阻塞該線程,只是將該線程轉入就緒狀態。
  • yield讓出CPU執行權給同等級的線程,如果沒有相同級別的線程在等待CPU的執行權,則該線程繼續執行。
發佈了105 篇原創文章 · 獲贊 63 · 訪問量 160萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章