這一次,讓咱們徹底掌握Java多線程(2/10)

多線程不只是Java後端開發面試中很是熱門的一個問題,也是各類高級工具、框架與分佈式的核心基石。可是這個領域相關的知識點涉及到了線程調度、線程同步,甚至在一些關鍵點上還涉及到了硬件原語、操做系統等更底層的知識。想要背背面試題很容易,可是若是面試官一追問就很容易露餡,更不用說真正想搞明白這個問題並應用在實際的代碼實踐中了。面試

不用擔憂!在接下來的一系列文章中將會由淺入深地貫穿這個問題的方方面面,雖然不如一些面試大全來得直接和速成。可是真正搞明白多線程編程不只可以一勞永逸地解決面試中的尷尬,並且還能打開通往底層知識的大門,不止是搞明白一個孤立的知識點,更是一個將之前曾經瞭解過的理論知識融會貫通連點成面的好機會。編程

雖然閱讀本文不須要事先了解併發相關的概念,可是若是已經掌握了一些大概的概念將會大大下降理解的難度。有興趣的讀者能夠參考本系列的第一篇文章來了解一下併發相關的基本概念——當咱們在說「併發、多線程」,說的是什麼?後端

這一系列文章將會包含10篇文章,本文是其中的第二篇,相信只要有耐心看完全部內容必定能輕鬆地玩轉多線程編程,不止是遊刃有餘地經過面試,更是能熟練掌握多線程編程的實踐技巧與併發實踐這一Java高級工具與框架的共同核心。bash

前五篇包含如下內容,將會在近期發佈:服務器

  1. 併發基本概念——當咱們在說「併發、多線程」,說的是什麼?
  2. 多線程入門——本文
  3. 線程池剖析
  4. 線程同步機制解析
  5. 併發常見問題

爲何要有多線程?

多線程程序和通常的單線程程序相比引入了同步、線程調度、內存可見性等一大堆複雜的問題,大大提升了開發者開發程序的難度,那麼爲何如今多線程在各個鄰域中還被如此趨之若鶩呢?微信

一種場景

在我大學的時候宿舍邊上有一家蓋澆飯,也提供炒菜。老闆很是地耿直,非要按點菜的順序一桌一桌地燒,若是前一桌的菜沒上完後一桌一個菜都別想吃到。結果就是天天這家店裏都是怨聲載道,顧客們經常等了半個小時也等不來一個菜填填肚子。你問我爲何還會有人去吃,受這罪,那確定是由於好吃啊😂。網絡

不過仔細想一想,好像通常的店裏好像並無這種狀況,由於大部分飯店都是混合着上的,就算前一桌沒上無缺歹會給幾個菜墊墊肚子。這在程序中也是同樣,不一樣的程序之間能夠交替運行,不至於在咱們的電腦上打開了開發工具就不能接收微信消息。數據結構

這就是多線程的一個應用場景:經過任務的交替執行使一臺計算機上能夠同時運行多個程序。多線程

另外一種場景

仍是在小飯館裏,一個服務員在給一桌點完菜以後確定不會等到這桌菜上完了纔去給另一桌點菜。通常都是點完菜就把訂單給了廚房,以後就繼續給下一桌點菜了。在這裏,咱們能夠把服務員想象成咱們的計算機,把廚房想象成遠程的服務器。那麼在咱們的電腦下載音樂的時候同時繼續播放音樂,這就能更高效地利用咱們的電腦了。併發

這種場景能夠描述爲:在等待網絡請求、磁盤I/O等耗時操做完成時,能夠用多線程來讓CPU繼續運轉,以達到有效利用CPU資源的目的。

最後一種場景

而後咱們來到了廚房,居然看到了一個大神,能一我的燒2個竈臺。若是這個廚師大神是一個多核處理器,那麼兩個竈臺就是兩個線程,若是隻給一個竈臺,那就浪費他的才能了,這絕對是一種損失。

這就是多線程應用的最後一種場景:將計算量比較大的任務拆分到兩個CPU上執行能夠減小執行完成的時間,而多線程就是拆分和執行任務的載體,沒有多線程就沒辦法把任務放到多個CPU上執行了。

什麼是多線程?

多線程就是不少線程的意思,嗯,是否是很簡單?

線程是操做系統中的一個執行單元,一樣的執行單元還有進程,全部的代碼都要在進程/線程中執行。線程是從屬於進程的,一個進程能夠包含多個線程。進程和線程之間還有一個區別就是,每一個進程有本身獨立的內存空間,互相直接不能直接訪問;可是同一個進程中的多個線程都共享進程的內存空間,因此能夠直接訪問同一塊內存,其中最典型的就是Java中的堆。

初識多線程編程

瞭解了這麼多理論概念,終於到了實際上手寫寫代碼的時候了。

建立線程

Java中的線程使用Thread類表示,Thread類的構造器能夠傳入一個實現了Runnable接口的對象,這個Runnable對象中的void run()方法就表明了線程中會執行的任務。例如若是要建立一個對整型變量進行自增的Runnable任務就能夠寫爲:

// 靜態變量,用於自增
private static int count = 0;

// 建立Runnable對象(匿名內部類對象)
Runnable task = new Runnable() {
    public void run() {
        for (int i = 0; i < 1e6; ++i) {
            count += 1;
    }
}
複製代碼

有了Runnable對象表明的待執行任務以後,咱們就能夠建立兩個線程來運行它了。

Thread t1 = new Thread(task);
Thread t2 = new Thread(task);
複製代碼

可是這時候只是建立了線程對象,實際上線程尚未被執行,想要執行線程還須要調用線程對象的start()方法。

t1.start();
t2.start();
複製代碼

這時候線程就能開始執行了,完整的代碼以下所示:

public class SimpleThread {

    private static int count = 0;

    public static void main(String[] args) throws Exception {
        Runnable task = new Runnable() {
            public void run() {
                for (int i = 0; i < 1000000; ++i) {
                    count = count + 1;
                }
            }
        };

        Thread t1 = new Thread(task);
        Thread t2 = new Thread(task);

        t1.start();
        t2.start();
        
        // 等待t1和t2執行完成
//        t1.join();
//        t2.join();

        System.out.println("count = " + count);
    }
}
複製代碼

最後輸出的結果是8251,你執行的時候應該會與這個值不一樣,可是同樣會遠遠小於一百萬。這好像離咱們指望的結果有點遠,畢竟每一個任務都累加了至少一百萬次。

這是由於咱們在main方法中建立線程並運行以後並無等待線程完成,使用t1.join()可使當前線程等待t1線程執行完成後再繼續執行。讓咱們去掉兩個join方法調用前面的雙斜槓試一試效果。

線程同步

在個人電腦上執行的結果是1753490,你執行的結果會有不一樣,可是一樣達不到咱們所指望的兩百萬。具體的緣由能夠從下面的執行順序圖中找到答案。

t1 t2
獲取count值爲0
獲取count值爲0
計算0+1的結果爲2
將2保存到count
計算0+1的結果爲2
將2保存到count

能夠看到,t1和t2兩個線程之間的併發運行會致使互相本身的結果覆蓋,最後的結果就會在一百萬與兩百萬之間,可是離兩百萬會有比較大的距離。這樣的多線程共同讀取並修改同一個共享數據的代碼區塊就被稱爲臨界區,臨界區同一時刻只容許一個線程進入,若是同時有多個線程進入就會致使數據競爭問題。若是有讀者對這裏提到的臨界區數據競爭概念還不清楚的,能夠參考本系列的第一篇介紹併發基本概念的文章——當咱們在說「併發、多線程」,說的是什麼?

在Java 5以前,咱們最經常使用的線程同步方式就是關鍵字synchronized,這個關鍵字既能夠標在方法上,也能夠做爲獨立的塊結構使用。方法聲明形式的synchronized關鍵字能夠在方法定義時如此使用:public synchronized static void methodName()。由於咱們的累加操做在繼承自Runnable接口的run()方法中,因此沒辦法改變方法的聲明,那麼就可使用以下的塊結構形式使用synchronized關鍵字:

Runnable task = new Runnable() {
    public void run() {
        for (int i = 0; i < 1000000; ++i) {
            synchronized (SimpleThread.class) {
                count += 1;
            }
        }
    }
};
複製代碼

synchronized是一種對象鎖,採用的鎖和具體的對象有關,若是是同一個對象就是同一個鎖;若是是不一樣的對象則是不一樣的鎖。同一時刻只能有一個線程持有鎖,也就意味着其餘想要獲取同一個鎖的線程會被阻塞,直到持有鎖的線程釋放這個鎖爲止。這裏能夠把對象鎖對應的對象看作是鎖的名稱,實現同步的並非對象自己,而是與對象對應的對象鎖。

在塊結構的synchronized關鍵字後的括號中的就是對象鎖所對應的對象,在上面的代碼中,咱們使用了SimpleThread類的類對象對應的鎖做爲同步工具。而若是synchronized關鍵字被用在方法聲明中,那麼若是是實例方法(非static方法)對應的對象就是this指針所指向的對象,若是是static方法,那麼對應的對象就是所處類的類對象。

此次咱們能夠看到輸出的結果每次都是穩定的兩百萬了,咱們成功完成了咱們的第一個完整的多線程程序🎉🎉🎉

後記

可是通常在實際編寫多線程代碼時,咱們通常不會直接建立Thread對象,而是使用線程池管理任務的執行。相信讀者們也在不少地方看見過「線程池」這個詞,若是但願瞭解線程池相關的使用與具體實現,能夠關注一下將會在近期發佈的下一篇文章。

到目前爲止,咱們都只是涉及了併發與多線程相關的概念和簡單的多線程程序實現。接下來咱們就會進入更深刻與複雜的多線程實現當中了,包括但不限於volatile關鍵字、CAS、AQS、內存可見性、經常使用線程池、阻塞隊列、死鎖、非死鎖併發問題、事件驅動模型等等知識點的應用和串聯,最後你們均可以逐步實如今各類工具中經常使用的一系列併發數據結構與程序,例如AtomicInteger、阻塞隊列、事件驅動Web服務器。相信你們經過這一系列多線程編程的冒險歷程以後必定能夠作到對多線程這個話題舉重若輕、有條不紊了。

相關文章
相關標籤/搜索