1. 程式人生 > >Java多執行緒程式設計學習與實踐

Java多執行緒程式設計學習與實踐

怎麼樣才算得上熟悉多執行緒程式設計?

第一,明白程序和執行緒的基本概念

第二,明白保護執行緒安全的基本方法有哪些

第三,明白這些執行緒安全的方法,包括互斥鎖,自旋鎖,無鎖程式設計的適用的業務場景是什麼?從OS和硬體角度說說原理是怎麼樣的?開銷在哪裡?

第四,能在現場藉助cas操作,風險指標實現無鎖的資料結構,比如無鎖棧,無鎖環形佇列。無鎖排序連結串列

下面就來學習一下多執行緒的知識然後進行實踐

此文只能說是java多執行緒的一個入門,Java裡執行緒知識很多完全可以寫一本書了,但是如果最基本的你都掌握不好,又怎麼能更上一個臺階呢?如果你覺得此文很簡單,那推薦你看看Java併發包的的執行緒池(Java併發程式設計與技術內幕:執行緒池深入理解
),或者看這個專欄:Java併發程式設計與技術內幕。你將會對Java裡頭的高併發場景下的執行緒有更加深刻的理解。本文主要講了java中多執行緒的使用方法、執行緒同步、執行緒資料傳遞、執行緒狀態及相應的一些執行緒函式用法、概述等。在這之前,首先讓我們來了解下在作業系統中程序和執行緒的區別

  程序:每個程序都有獨立的程式碼和資料空間(程序上下文),程序間的切換會有較大的開銷,一個程序包含1--n個執行緒。(程序是資源分配的最小單位)

  執行緒:同一類執行緒共享程式碼和資料空間,每個執行緒有獨立的執行棧和程式計數器(PC),執行緒切換開銷小。(執行緒是cpu排程的最小單位)

  執行緒和程序一樣分為五個階段:建立、就緒、執行、阻塞、終止。

  多程序是指作業系統能同時執行多個任務(程式)。

  多執行緒是指在同一程式中有多個順序流在執行。

java中要想實現多執行緒,有兩種手段,一種是繼續Thread類,另外一種是實現

一、繼承java.lang.Thread類

繼承Thread類的方法是比較常用的一種,如果說你只是想起一條執行緒。沒有什麼其它特殊的要求,那麼可以使用Thread.(推薦使用Runable,後頭會說明為什麼)。下面來看一個簡單的例項

/**
* @author leon
* @createDate 2018年7月4日 下午5:13:25
* @version v1.0
* @classRemarks 多執行緒學習
*/
public class ThreadTest  extends Thread{
    
    private String name;
    
    public ThreadTest(String name) {
        super();
        this.name = name;
    }

    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {  
            System.out.println(name + "執行  :  " + i);  
            try {  
                sleep((int) Math.random() * 10);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
    }

    public static void main(String[] args) {
        ThreadTest mTh1=new ThreadTest("A");  
        ThreadTest mTh2=new ThreadTest("B");  
        mTh1.start();  
        mTh2.start();
    }
}

執行後輸出結果:


說明:程式啟動執行main時候,java虛擬機器啟動一個程序,主執行緒main在main()呼叫時候被建立。隨著呼叫MitiSay的兩個物件的start方法,另外兩個執行緒也啟動了,這樣,整個應用就在多執行緒下執行。注意start()方法的呼叫後並不是立即執行多執行緒程式碼,而是使得該執行緒變為可執行態(Runnable),什麼時候執行是由作業系統決定的。從程式執行的結果可以發現,多執行緒程式是亂序執行。因此,只有亂序執行的程式碼才有必要設計為多執行緒。Thread.sleep()方法呼叫目的是不讓當前執行緒獨自霸佔該程序所獲取的CPU資源,以留出一定時間給其他執行緒執行的機會。實際上所有的多執行緒程式碼執行順序都是不確定的,每次執行的結果都是隨機的。

但是start方法重複呼叫的話,會出現java.lang.IllegalThreadStateException異常。

二、實現java.lang.Runnable 介面

採用Runnable也是非常常見的一種,我們只需要重寫run方法即可。下面也來看個例項。

執行結果如下


說明:RunnableTest類通過實現Runnable介面,使得該類有了多執行緒類的特徵。run()方法是多執行緒程式的一個約定。所有的多執行緒程式碼都在run方法裡面。Thread類實際上也是實現了Runnable介面的類。在啟動的多執行緒的時候,需要先通過Thread類的構造方法Thread(Runnable target) 構造出物件,然後呼叫Thread物件的start()方法來執行多執行緒程式碼。實際上所有的多執行緒程式碼都是通過執行Thread的start()方法來執行的。因此,不管是擴充套件Thread類還是實現Runnable介面來實現多執行緒,最終還是通過Thread的物件的API來控制執行緒的,熟悉Thread類的API是進行多執行緒程式設計的基礎。

三、Thread和Runnable的區別

如果一個類繼承Thread,則不適合資源共享。但是如果實現了Runable介面的話,則很容易的實現資源共享。

總結:

實現Runnable介面比繼承Thread類所具有的優勢:

1)適合多個相同的程式程式碼的執行緒去處理同一個資源

2)可以避免java中的單繼承的限制

3)增加程式的健壯性,程式碼可以被多個執行緒共享,程式碼和資料獨立

4)執行緒池只能放入實現Runable或callable類執行緒,不能直接放入繼承Thread的類

提醒一下大家:main方法其實也是一個執行緒。在java中所有的執行緒都是同時啟動的,至於什麼時候,哪個先執行,完全看誰先得到CPU的資源。

在java中,每次程式執行至少啟動2個執行緒。一個是main執行緒,一個是垃圾收集執行緒。因為每當使用java命令執行一個類的時候,實際上都會啟動一個JVM,每一個JVM其實就是在作業系統中啟動了一個程序。

四、執行緒狀態轉換

下面的這個圖非常重要!你如果看懂了這個圖,那麼對於多執行緒的理解將會更加深刻!

1、新建狀態(New):新建立了一個執行緒物件。2、就緒狀態(Runnable):執行緒物件建立後,其他執行緒呼叫了該物件的start()方法。該狀態的執行緒位於可執行執行緒池中,變得可執行,等待獲取CPU的使用權。3、執行狀態(Running):就緒狀態的執行緒獲取了CPU,執行程式程式碼。4、阻塞狀態(Blocked):阻塞狀態是執行緒因為某種原因放棄CPU使用權,暫時停止執行。直到執行緒進入就緒狀態,才有機會轉到執行狀態。阻塞的情況分三種:(一)、等待阻塞:執行的執行緒執行wait()方法,JVM會把該執行緒放入等待池中。(wait會釋放持有的鎖)(二)、同步阻塞:執行的執行緒在獲取物件的同步鎖時,若該同步鎖被別的執行緒佔用,則JVM會把該執行緒放入鎖池中。(三)、其他阻塞:執行的執行緒執行sleep()或join()方法,或者發出了I/O請求時,JVM會把該執行緒置為阻塞狀態。當sleep()狀態超時、join()等待執行緒終止或者超時、或者I/O處理完畢時,執行緒重新轉入就緒狀態。(注意,sleep是不會釋放持有的鎖)5、死亡狀態(Dead):執行緒執行完了或者因異常退出了run()方法,該執行緒結束生命週期。

五、執行緒排程

1、調整執行緒優先順序:Java執行緒有優先順序,優先順序高的執行緒會獲得較多的執行機會。Java執行緒的優先順序用整數表示,取值範圍是1~10,Thread類有以下三個靜態常量:static int MAX_PRIORITY          執行緒可以具有的最高優先順序,取值為10。static int MIN_PRIORITY          執行緒可以具有的最低優先順序,取值為1。static int NORM_PRIORITY          分配給執行緒的預設優先順序,取值為5。Thread類的setPriority()和getPriority()方法分別用來設定和獲取執行緒的優先順序。每個執行緒都有預設的優先順序。主執行緒的預設優先順序為Thread.NORM_PRIORITY。執行緒的優先順序有繼承關係,比如A執行緒中建立了B執行緒,那麼B將和A具有相同的優先順序。JVM提供了10個執行緒優先順序,但與常見的作業系統都不能很好的對映。如果希望程式能移植到各個作業系統中,應該僅僅使用Thread類有以下三個靜態常量作為優先順序,這樣能保證同樣的優先順序採用了同樣的排程方式。2、執行緒睡眠:Thread.sleep(long millis)方法,使執行緒轉到阻塞狀態。millis引數設定睡眠的時間,以毫秒為單位。當睡眠結束後,就轉為就緒(Runnable)狀態。sleep()平臺移植性好。3、執行緒等待:Object類中的wait()方法,導致當前的執行緒等待,直到其他執行緒呼叫此物件的 notify() 方法或 notifyAll() 喚醒方法。這個兩個喚醒方法也是Object類中的方法,行為等價於呼叫 wait(0) 一樣。4、執行緒讓步:Thread.yield() 方法,暫停當前正在執行的執行緒物件,把執行機會讓給相同或者更高優先順序的執行緒。5、執行緒加入:join()方法,等待其他執行緒終止。在當前執行緒中呼叫另一個執行緒的join()方法,則當前執行緒轉入阻塞狀態,直到另一個程序執行結束,當前執行緒再由阻塞轉為就緒狀態。6、執行緒喚醒:Object類中的notify()方法,喚醒在此物件監視器上等待的單個執行緒。如果所有執行緒都在此物件上等待,則會選擇喚醒其中一個執行緒。選擇是任意性的,並在對實現做出決定時發生。執行緒通過呼叫其中一個 wait 方法,在物件的監視器上等待。 直到當前的執行緒放棄此物件上的鎖定,才能繼續執行被喚醒的執行緒。被喚醒的執行緒將以常規方式與在該物件上主動同步的其他所有執行緒進行競爭;例如,喚醒的執行緒在作為鎖定此物件的下一個執行緒方面沒有可靠的特權或劣勢。類似的方法還有一個notifyAll(),喚醒在此物件監視器上等待的所有執行緒。 注意:Thread中suspend()和resume()兩個方法在JDK1.5中已經廢除,不再介紹。因為有死鎖傾向。

六、執行緒常用函式說明

①sleep(long millis): 在指定的毫秒數內讓當前正在執行的執行緒休眠(暫停執行)

②join():指等待t執行緒終止。

使用方式。

join是Thread類的一個方法,啟動執行緒後直接呼叫,即join()的作用是:“等待該執行緒終止”,這裡需要理解的就是該執行緒是指的主執行緒等待子執行緒的終止。也就是在子執行緒呼叫了join()方法後面的程式碼,只有等到子執行緒結束了才能執行。

比如 Thread t = new AThread(); t.start(); t.join();

為什麼要用join()方法

在很多情況下,主執行緒生成並起動了子執行緒,如果子執行緒裡要進行大量的耗時的運算,主執行緒往往將於子執行緒之前結束,但是如果主執行緒處理完其他的事務後,需要用到子執行緒的處理結果,也就是主執行緒需要等待子執行緒執行完成之後再結束,這個時候就要用到join()方法了。