Java 線程和多線程執行過程分析

本文目錄:
1.幾個基本的概念
2.建立線程的兩種方法
3.線程相關的經常使用方法
4.多線程安全問題和線程同步
 4.1 多線程安全問題
 4.2 線程同步
 4.3 同步代碼塊和同步函數的區別以及鎖是什麼
 4.4 單例懶漢模式的多線程安全問題
5.死鎖(DeadLock) javascript

1.幾個基本的概念

本文涉及到的一些概念,有些是基礎知識,有些在後文會展開詳細的說明。php

  1. 進程(Process):一個程序運行起來時在內存中開闢一段空間用來運行程序,這段空間包括heap、stack、data segment和code segment。例如,開一個QQ就代表開了一個QQ進程。
  2. 線程(Thread):每個進程中都至少有一個線程。線程是指程序中代碼運行時的運行路徑,一個線程表示一條路徑。例如QQ進程中,發送消息、接收消息、接收文件、發送文件等各類獨立的功能都須要一個線程來執行。
  3. 進程和線程的區別:從資源的角度來考慮,進程主要考慮的是CPU和內存,而線程主要考慮的是CPU的調度,某進程中的各線程之間能夠共享這個進程的不少資源。
    從粒度粗細來考慮,進程的粒度較粗,進程上下文切換時消耗的CPU資源較多。線程的粒度要小的多,雖然線程也會切換,但由於共享進程的上下文,相比進程上下文切換而言,同進程內的線程切換時消耗的資源要小的多的多。在JAVA中,除了java運行時啓動的JVM是一個進程,其餘全部任務都以線程的方式執行,也就是說java應用程序是單進程的,甚至能夠說沒有進程的概念。
  4. 線程組(ThreadGroup):線程組提供了一些批量管理線程的方法,所以經過將線程加入到線程組中,能夠更方便地管理這些線程。
  5. 線程的狀態:就緒態、運行態、睡眠態。還能夠分爲存活和死亡,死亡表示線程結束,非死亡則存活,所以存活包含就緒、運行、睡眠。
  6. 中斷睡眠(interrupt):將線程從睡眠態強制喚醒,喚醒後線程將進入就緒隊列等待cpu調度。
  7. 併發操做:多個線程同時操做一個資源。這會帶來多線程安全問題,解決方法是使用線程同步。
  8. 線程同步:讓線程中的某些任務原子化,即要麼所有執行完畢,要麼不開始執行。經過互斥鎖來實現同步,經過監視這個互斥鎖是否被誰持有來決定是否從睡眠態轉爲就緒態(即從線程池中出去),也就是是否有資格去獲取cpu的執行權。線程同步解決了線程安全的問題,但下降了程序的效率。
  9. 死鎖:線程全睡眠了沒法被喚醒,致使程序卡死在某一處沒法再執行下去。典型的是兩個同步線程,線程1持有A鎖,且等待B鎖,但線程2持有B鎖且等待A鎖,這樣的僵局會形成死鎖。但須要注意的是,死鎖並不是都是由於僵局,只要兩邊的線程都沒法繼續向下執行代碼(或者兩邊的線程池都沒法被喚醒,這是等價的概念,由於鎖等待也會讓進程進入睡眠態),則都是死鎖

還需須要明確的一個關鍵點是:CPU對就緒隊列中每一個線程的調度是隨機的(對咱們人類來講),且分配的時間片也是隨機的(對人類來講)。css

2.建立線程的兩種方法

Java中有兩種建立線程的方式。html

建立線程方式一: java

  1. 繼承Thread類(在java.lang包中),並重寫該類的run()方法,其中run()方法即線程須要執行的任務代碼。
  2. 而後new出這個類對象。這表示建立線程對象。
  3. 調用start()方法開啓線程來執行任務(start()方法會調用run()以便執行任務)。

例以下面的代碼中,在主線程main中建立了兩個線程對象,前後並前後調用start()開啓這兩個線程,這兩個線程會各自執行MyThread中的run()方法。nginx

class MyThread extends Thread {
    String name;
    String gender;

    MyThread(String name,String gender){
        this.name = name;
        this.gender = gender;
    }

    public void run(){
        int i = 0;
        while(i<=20) {
            //除了主線程main,其他線程從0開始編號,currentThread()獲取的是當前線程對象
            System.out.println(Thread.currentThread().getName()+"-----"+i+"------"+name+"------"+gender);
            i++;
        }
    }
}

public class CreateThread {
    public static void main(String[] args) {
        MyThread mt1 = new MyThread("malong","Male");
        MyThread mt2 = new MyThread("Gaoxiao","Female");

        mt1.start();
        mt2.start();
        System.out.println("main thread over");
    }
}

上面的代碼執行時,有三個線程,首先是主線程main建立2個線程對象,並開啓這兩個線程任務,開啓兩個線程後主線程輸出"main thread over",而後main線程結束。在開啓兩個線程任務後,這兩個線程加入到了就緒隊列等待CPU的調度執行。以下圖。由於每一個線程被cpu調度是隨機的,執行時間也是隨機的,因此即便mt1先開啓任務,但mt2可能會比mt1線程先執行,也可能更先消亡。git

建立線程方式二:github

  1. 實現Runnable接口,並重寫run()方法。
  2. 建立子類對象。
  3. 建立Thread對象來建立線程對象,並將實現了Runnable接口的對象做爲參數傳遞給Thread()構造方法。
  4. 調用start()方法開啓線程來執行run()中的任務。
class MyThread implements Runnable {
    String name;
    String gender;

    MyThread(String name,String gender){
        this.name = name;
        this.gender = gender;
    }

    public void run(){
        int i = 0;
        while(i<=200) {
            System.out.println(Thread.currentThread().getName()+"-----"+i);
            i++;
        }
    }
}

public class CreateThread2 {
    public static void main(String[] args) {
        //建立子類對象
        MyThread mt = new MyThread("malong","Male");
        //建立線程對象
        Thread th1 = new Thread(mt);
        Thread th2 = new Thread(mt);

        th1.start();
        th2.start();
        System.out.println("main thread over");
    }
}

這兩種建立線程的方法,無疑第二種(實現Runnable接口)要好一些,由於第一種建立方法繼承了Thread後就沒法繼承其餘父類。web

3.線程相關的經常使用方法

Thread類中的方法:django

  • isAlive():判斷線程是否還活着。活着的概念是指是否消亡了,對於運行態、就緒態、睡眠態的線程都是活着的狀態。
  • currentThread():返回值爲Thread,返回當前線程對象。
  • getName():獲取當前線程的線程名稱。
  • setName():設置線程名稱。給線程命名還可使用構造方法Thread(String thread_name)Thread(Runnable r,String thread_name)
  • getPriority():獲取線程優先級。優先級範圍值爲1-10(默認值爲5),相鄰值之間的差距對cpu調度的影響很小。通常使用3個字段MIN_PRIORITY、NORM_PRIORITY、MAX_PRIORITY分別表示一、五、10三個優先級,這三個優先級可較大地區分cpu的調度。
  • setPriority():設置線程優先級。
  • run():封裝的是線程開啓後要執行的任務代碼。若是run()中沒有任何代碼,則線程不作任何事情。
  • start():開啓線程並讓線程開始執行run()中的任務。
  • toString():返回線程的名稱、優先級和線程組。
  • sleep(long millis):讓線程睡眠多少毫秒。
  • join(t1):將線程t1合併到當前線程,並等待線程t1執行完畢後才繼續執行當前線程。即讓t1線程強制插隊到當前線程的前面並等待t1完成。
  • yield():將當前正在執行的線程退讓出去,以讓就緒隊列中的其餘線程有更大的概率被cpu調度。即強制本身放棄cpu,並將本身放入就緒隊列。因爲本身也在就緒隊列中,因此即便此刻本身放棄了cpu,下一次仍是可能會當即被cpu選中調度。但畢竟給了機會給其它就緒態線程,因此其餘就緒態線程被選中的概率要更大一些。

Object類中的方法:

  • wait():線程進入某個線程池中並進入睡眠態。等待notify()或notifyAll()的喚醒。
  • notify():從某個線程池中隨機喚醒一個睡眠態的線程。
  • notifyAll():喚醒某個線程池中全部的睡眠態線程。

這裏的某個線程池是由鎖對象決定的。持有相同鎖對象的線程屬於同一個線程池。見後文。

通常來講,wait()和喚醒的notify()或notifyAll()是成對出現的,不然很容易出現死鎖。

sleep()和wait()的區別:(1)所屬類不一樣:sleep()在Thread類中,wait()則是在Object中;(2)sleep()能夠指定睡眠時間,wait()雖然也能夠指定睡眠時間,但大多數時候都不會去指定;(3)sleep()不會拋異常,而wait()會拋異常;(4)sleep()能夠在任何地方使用,而wait()必須在同步代碼塊或同步函數中使用;(5)最大的區別是sleep()睡眠時不會釋放鎖,不會進入特定的線程池,在睡眠時間結束後自動甦醒並繼續往下執行任務,而wait()睡眠時會釋放鎖,進入線程池,等待notify()或notifyAll()的喚醒。

java.util.concurrent.locks包中的類和它們的方法:

  • Lock類中:

    • lock():獲取鎖(互斥鎖)。
    • unlock():釋放鎖。
    • newCondition():建立關聯此lock對象的Condition對象。
  • Condition類中:

    • await():和wait()同樣。
    • signal():和notify()同樣。
    • signalAll():和notifyAll()同樣。

4.多線程安全問題和線程同步

4.1 多線程安全問題

線程安全問題是指多線程同時執行時,對同一資源的併發操做會致使資源數據的混亂。

例以下面是用多個線程(窗口)售票的代碼。

class Ticket implements Runnable {
    private int num;    //票的數量

    Ticket(int num){
        this.num = num;
    }

    //售票
    public void sale() {
        if(num>0) {
            num--;
            System.out.println(Thread.currentThread().getName()+"-------"+remain());
        }
    }

    //獲取剩餘票數
    public int remain() {
        return num;
    }

    public void run(){
        while(true) {
            sale();
        }
    }
}

public class ConcurrentDemo {
    public static void main(String[] args) {
        Ticket t = new Ticket(100);
        //建立多個線程對象
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);
        Thread t3 = new Thread(t);
        Thread t4 = new Thread(t);

        //開啓多個線程使其執行任務
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

執行結果大體以下:

以上代碼的執行過程大體以下圖:

共開啓了4個線程執行任務(不考慮main主線程),每個線程都有4個任務:

  • ①判斷if條件if(num>0);
  • ②票數自減num--;
  • ③獲取剩餘票數return num;
  • ④打印返回的num數量System.out.println(Thread.currentThread().getName()+"-------"+remain())

這四個任務的共同點也是關鍵點在於它們都操做同一個資源Ticket對象中的num,這是多線程出現安全問題的本質,也是分析多線程執行過程的切入點

當main線程開啓t1-t4這4個線程時,它們首先進入就緒隊列等待被CPU隨機選中。(1).假如t1被先選中,分配的時間片執行到任務②就結束了,因而t1進入就緒隊列等待被CPU隨機選中,此時票數num自減後爲99;(2).當t3被CPU選中時,t3所讀取到的num也爲99,假如t3分配到的時間片在執行到任務②也結束了,此時票數num自減後爲98;(3).同理t2被選中執行到任務②結束後,num爲97;(4).此時t3又被選中了,因而能夠執行任務③,甚至是任務④,假設執行完任務④時間片才結束,因而t3的打印語句打印出來的num結果爲97;(5).t1又被選中了,因而任務④打印出來的num也爲97。

顯然,上面的代碼有幾個問題:(1)有些票沒有賣出去了可是沒有記錄;(2)有的票重複賣了。這就是線程安全問題。

4.2 線程同步

java中解決線程安全問題的方法是使用互斥鎖,也可稱之爲"同步"。解決思路以下:

(1).爲待執行的任務設定給定一把鎖,擁有相同鎖對象的線程在wait()時會進入同一個線程池睡眠。
(2).線程在執行這個設了鎖的任務時,首先判斷鎖是否空閒(即鎖處於釋放狀態),若是空閒則去持有這把鎖,只有持有這把鎖的線程才能執行這個任務。即便時間片到了,它也不是釋放鎖,只有wait()或線程結束時纔會安全地釋放鎖。
(3).這樣一來,鎖被某個線程持有時,其餘線程在鎖判斷後就繼續會線程池睡眠去了(或就緒隊列)。最終致使的結果是,(設計合理的狀況下)某個線程必定完整地執行完一個任務,其餘線程纔有機會去持有鎖並執行任務。

換句話說,使用同步線程,能夠保證線程執行的任務具備原子性,只要某個同步任務開始執行了就必定執行結束,且不容許其餘線程參與。

讓線程同步的方式有兩種,一種是使用synchronized(){}代碼塊,一種是使用synchronized關鍵字修飾待保證同步的方法。

class Ticket implements Runnable {
    private int num;    //初始化票的數量
    private Object obj = new Object();

    Ticket(int num){
        this.num = num;
    }

    //售票
    public void sale() {
        synchronized(obj) {   //使用同步代碼塊封裝須要保證原子性的代碼
            if(num>0) {
                num--;
                System.out.println(Thread.currentThread().getName()+"-------"+remain());
            }
        }
    }

    //獲取剩餘票數
    public int remain() {
        return num;
    }

    public void run(){
        while(true) {
            sale();
        }
    }
}
class Ticket implements Runnable {
    private int num;    //初始化票的數量

    Ticket(int num){
        this.num = num;
    }

    public synchronized void sale() {  //使用synchronized關鍵字,方法變爲同步方法
        if(num>0) {
            num--;
            System.out.println(Thread.currentThread().getName()+"-------"+remain());
        }
    }

    //獲取剩餘票數
    public int remain() {
        return num;
    }

    public void run(){
        while(true) {
            sale();
        }
    }
}

使用同步以後,if(num>0)num--return numprint(num)這4個任務就強制具備原子性。某個線程只要開始執行了if語句,它就必定會繼續執行直到執行完print(num),纔算完成了一整個任務。只有完成了一整個任務,線程纔會釋放鎖(固然,也可能繼續判斷while(true)並進入下一個循環)。

4.3 同步代碼塊和同步函數的區別以及鎖是什麼

前面的示例中,同步代碼塊synchronized(obj){}中傳遞了一個obj的Object對象,這個obj能夠是任意一個對象的引用,這些引用傳遞給代碼塊的做用是爲了標識這個同步任務所屬的鎖。

synchronized函數的本質實際上是使用了this做爲這個同步函數的鎖標識,this表明的是當前對象的引用。但若是同步函數是靜態的,即便用了static修飾,則此時this還沒出現,它使用的鎖是"類名.class"這個字節碼文件對象,對於java來講,這也是一個對象,並且一個類中必定有這個對象。

使用相同的鎖之間會互斥,但不一樣鎖之間則沒有任何影響。所以,要保證任務同步(原子性),這些任務所關聯的鎖必須相同。也所以,若是有多個同步任務(各自保證本身的同步性),就必定不能都使用同步函數。

例以下面的例子中,寫了兩個相同的sale()方法,而且使用了flag標記讓不一樣線程能執行這兩個同步任務。若是出現了多線程安全問題,則代表synchronized函數和同步代碼塊使用的是不一樣對象鎖。若是將同步代碼塊中的對象改成this後不出現多線程安全問題,則代表同步函數使用的是this對象。若是爲sale2()加上靜態修飾static,則將obj替換爲"Ticket.class"來測試。

class Ticket implements Runnable {
    private int num;    //初始化票的數量
    boolean flag = true;
    private Object obj = new Object();

    Ticket(int num){
        this.num = num;
    }

    //售票
    public void sale1() {
        synchronized(obj) {  //使用的是obj標識鎖
            if(num>0) {
                num--;
                try{Thread.sleep(1);} catch (InterruptedException i){}  //爲了確保num--和println()分開,加上sleep
                System.out.println(Thread.currentThread().getName()+"===sale1==="+remain());
            }
        }
    }

    public synchronized void sale2() {   //使用this標識鎖
        if(num>0) {
            num--;
            try{Thread.sleep(1);} catch (InterruptedException i){}
            System.out.println(Thread.currentThread().getName()+"===sale2==========="+remain());
        }
    }

    //獲取剩餘票數
    public int remain() {
        return num;
    }

    public void run(){
        if(flag){
            while(true) {
                sale1();
            }
        } else {
            while(true) {
                sale2();
            }
        }
    }
}

public class Mytest {
    public static void main(String[] args) {
        Ticket t = new Ticket(200);
        //建立多個線程對象
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);

        //開啓多個線程使其執行任務
        t1.start();
        try{Thread.sleep(1);} catch (InterruptedException i){}
        t.flag = false;
        t2.start();
    }
}

如下是執行結果中的一小片斷,出現了多線程安全問題。而若是將同步代碼塊中的obj改成this,則不會出現多線程安全問題。

Thread-0===sale1===197
Thread-1===sale2===========197
Thread-0===sale1===195
Thread-1===sale2===========195
Thread-1===sale2===========193
Thread-0===sale1===193
Thread-0===sale1===191
Thread-1===sale2===========191

4.4 單例懶漢模式的多線程安全問題

單例餓漢式:

class Single {
    private static final Single s = new Single();
    private Single(){};
    public static Single getInstance() {
        return s;
    }
}

單例懶漢式:

class Single {
    private static Single s = null;
    private Single(){};
    public static getInstance(){
        if(s==null) {
            s = new Single();
        }
        return s;
    }
}

當多線程操做單例餓漢式和懶漢式對象的資源時,是否有多線程安全問題?

class Demo implements Runnable {
    public void run(){
        Single.getInstance();
    }
}

以上面的代碼爲例。當多線程分別被CPU調度時,餓漢式中的getInstance()返回的s,s是final屬性修飾的,所以隨便哪一個線程訪問都是固定不變的。而懶漢式則隨着不一樣線程的來臨,不斷new Single(),也就是說各個線程獲取到的對象s是不一樣的,存在多線程安全問題。

只需使用同步就能夠解決懶漢式的多線程安全問題。例如使用同步方法。

class Single {
    private static Single s = null;
    private Single(){};
    public static synchronized getInstance(){
        if (s == null){
            s = new Single();
        }
        return s;
    }
}

這樣一來,每一個線程來執行這個任務時,都將先判斷Single.class這個對象標識的鎖是否已經被其餘線程持有。雖然解決了問題,但由於每一個線程都額外地判斷一次鎖,致使效率有所降低。能夠採用下面的雙重判斷來解決這個效率下降問題。

class Single {
    private static Single s = null;
    private Single(){};
    public static getInstance(){
        if (s == null) {
            synchronized(Single.class){
                if (s == null){
                    s = new Single();
                }
                return s;
            }
        }
    }
}

這樣一來,當第一個線程執行這個任務時,將判斷s==null爲true,因而執行同步代碼塊並持有鎖,保證任務的原子性。並且,即便在最初判斷s==null後切換到其餘線程了,也沒有關係,由於總有一個線程會執行到同步代碼塊並持有鎖,只要持有鎖了就必定執行s= new Single(),在這以後,全部的線程在第一階段的"s==null"判斷都爲false,從而提升效率。其實,雙重判斷的同步懶漢式的判斷次數和餓漢式的判斷次數幾乎相等。

5.死鎖(DeadLock)

最典型的死鎖是僵局問題,A等B,B等A,誰都不釋放,形成僵局,最後兩個線程都沒法執行下去。

例以下面的代碼示例,sale1()中,obj鎖須要持有this鎖才能完成任務總體,而sale2()中,this鎖須要持有obj鎖才能完成任務總體。當兩個線程都開始執行任務後,就開始產生死鎖問題。

class Ticket implements Runnable {
    private int num;    
    boolean flag = true;
    private Object obj = new Object();

    Ticket(int num){
        this.num = num;
    }


    public void sale1() {
        synchronized(obj) {   //obj鎖
            sale2();          //this鎖
        }
    }

    public synchronized void sale2() {   //this鎖
        synchronized(obj){               //obj鎖
            if(num>0) {
                num--;
                try{Thread.sleep(1);} catch (InterruptedException i){}
                System.out.println(Thread.currentThread().getName()+"========="+remain());
            }
        }
    }

    //獲取剩餘票數
    public int remain() {
        return num;
    }

    public void run(){
        if(flag){
            while(true) {
                sale1();
            }
        } else {
            while(true) {
                sale2();
            }
        }
    }
}

public class DeadLockDemo {
    public static void main(String[] args) {
        Ticket t = new Ticket(200);
        //建立多個線程對象
        Thread t1 = new Thread(t);
        Thread t2 = new Thread(t);

        //開啓多個線程使其執行任務
        t1.start();
        try{Thread.sleep(1);} catch (InterruptedException i){}
        t.flag = false;
        t2.start();
    }
}

爲了不死鎖,儘可能不要在同步中嵌套同步,由於這樣很容易形成死鎖。

 

注:若您以爲這篇文章還不錯請點擊右下角推薦,您的支持能激發做者更大的寫做熱情,很是感謝!

相關文章
相關標籤/搜索