多線程

線程與進(jìn)程

進(jìn)程:每個(gè)進(jìn)程都有獨(dú)立的代碼和數(shù)據(jù)空間(進(jìn)程上下文),進(jìn)程間的切換會有較大的開銷,一般來說進(jìn)程之間不允許相互通信,一個(gè)進(jìn)程包含1~n個(gè)線程。(進(jìn)程是資源分配的最小單位)
線程:同一類線程共享代碼和數(shù)據(jù)空間,每個(gè)線程都有獨(dú)立的運(yùn)行棧和程序計(jì)數(shù)器(PC),線程間的切換開銷較小,線程之間允許相互通信。(線程是CPU調(diào)度的最小單位)
線程和進(jìn)程一樣分為五個(gè)階段:創(chuàng)建、就緒、運(yùn)行、阻塞、終止。
多進(jìn)程是指操作系統(tǒng)能同時(shí)運(yùn)行多個(gè)程序。
多線程是指在同一程序中有多個(gè)順序流在執(zhí)行,但多線程沒有提高效率,而是提高了資源的利用率。
一個(gè)程序至少有一個(gè)進(jìn)程,一個(gè)進(jìn)程至少有一個(gè)線程。
并行:多個(gè)CPU或者多臺機(jī)器同時(shí)處理一段邏輯,是真正的同時(shí)。
并發(fā):通過CPU調(diào)度算法,讓用戶看上去是在同時(shí)執(zhí)行,實(shí)際上從CPU操作層面來看并不是真正的同時(shí),而是通過CPU的調(diào)度,在不同的任務(wù)之間進(jìn)行高速切換。

繼承java.lang.Thread類

繼承Thread類是比較常用的一種方法,如果說只是想創(chuàng)建一條新的線程,沒有什么其它要求,那么可以選擇使用繼承Thread類,并重寫run()方法。下面來看一個(gè)簡單的實(shí)例:

package leif;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        MyThread myThread1 = new MyThread("A");
        MyThread myThread2 = new MyThread("B");
        myThread1.start();
        myThread2.start();
    }
}

class MyThread extends Thread {
    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            System.out.println(getName() + "線程運(yùn)行:" + i);

            try {
                sleep(i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
image.png

程序啟動時(shí),JVM會啟動一個(gè)進(jìn)程,主線程在main()方法調(diào)用時(shí)被創(chuàng)建。隨著調(diào)用MyThread的兩個(gè)對象的start()方法,另外兩個(gè)線程將啟動,這樣整個(gè)程序就在多線程下運(yùn)行。
調(diào)用start()方法后并不會立即變成運(yùn)行狀態(tài)(Running),而是使該線程變?yōu)榫途w狀態(tài)(Runnable),什么時(shí)候運(yùn)行是由操作系統(tǒng)調(diào)度決定的。
從程序運(yùn)行的結(jié)果可以發(fā)現(xiàn),多個(gè)線程是亂序執(zhí)行的。因此,只有亂序執(zhí)行的程序才有必要設(shè)計(jì)成多線程。
調(diào)用Thread.sleep()方法的目的是不讓當(dāng)前線程獨(dú)自霸占該進(jìn)程所獲取的CPU資源,以留出一定時(shí)間讓其他線程有運(yùn)行的機(jī)會。
實(shí)際上所有的線程執(zhí)行順序都是不確定的,所以每次執(zhí)行的結(jié)果都是隨機(jī)的。
如果start()方法重復(fù)調(diào)用的話,就會出現(xiàn)java.lang.IllegalThreadStateException異常。

實(shí)現(xiàn)java.lang.Runnable接口

實(shí)現(xiàn)Runnable接口也是創(chuàng)建線程非常常見的一種方式,同樣只需要重寫run()方法即可。下面也來看個(gè)實(shí)例:

package leif;

public class Test {
    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(new MyThread(), "C");
        Thread thread2 = new Thread(new MyThread(), "D");
        thread1.start();
        thread2.start();
    }
}

class MyThread implements Runnable {
    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            System.out.println(Thread.currentThread().getName() + "線程運(yùn)行:" + i);

            try {
                Thread.sleep(i);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}
image.png

通過實(shí)現(xiàn)Runnable接口,使得該類有了多線程類的特征。run()方法是多線程程序的一個(gè)約定,所有的多線程代碼都在run()方法里。Thread類其實(shí)也是實(shí)現(xiàn)了Runnable接口的類。
在啟動多線程的時(shí)候,需要先通過Thread類的構(gòu)造方法構(gòu)造出線程對象,然后調(diào)用線程對象的start()方法來啟動該線程。
實(shí)際上所有的線程都是通過調(diào)用Thread類的start()方法來運(yùn)行的,因此,不管是繼承Thread類還是實(shí)現(xiàn)Runnable接口來實(shí)現(xiàn)多線程,最終還是要通過Thread對象的API來控制線程,所以熟悉Thread類的API是進(jìn)行多線程編程的基礎(chǔ)。

Thread和Runnable的區(qū)別

如果一個(gè)類繼承Thread,則不適合實(shí)現(xiàn)資源共享,但是如果實(shí)現(xiàn)了Runable接口的話,則可以很容易的實(shí)現(xiàn)資源共享。
實(shí)現(xiàn)Runnable接口比繼承Thread類所具有的優(yōu)勢:

  1. 增強(qiáng)程序的健壯性,資源可以被多個(gè)線程共享
  2. 可以避免單繼承限制
  3. 線程池只能放入實(shí)現(xiàn)Runable或Callable接口的線程,不能直接放入繼承Thread類的線程

main()方法其實(shí)也是一個(gè)線程,每次程序運(yùn)行至少啟動兩個(gè)線程,一個(gè)是main線程,一個(gè)是垃圾回收線程。在Java中哪個(gè)線程先執(zhí)行,完全取決于誰先搶到CPU的資源。

線程的生命周期

image.png
  • 初試狀態(tài)(New):創(chuàng)建了一個(gè)新的線程
  • 就緒狀態(tài)(Runnable):線程創(chuàng)建后,調(diào)用該線程的start()方法,該線程就會由初始狀態(tài)變?yōu)榫途w狀態(tài),該狀態(tài)的線程位于可運(yùn)行線程池中,等待獲取CPU的使用權(quán)。
  • 運(yùn)行狀態(tài)(Running):就緒狀態(tài)的線程獲取了CPU的使用權(quán),變?yōu)檫\(yùn)行狀態(tài),并開始執(zhí)行程序代碼
  • 阻塞狀態(tài)(Blocked):阻塞狀態(tài)是線程因?yàn)槟撤N原因放棄了CPU的使用權(quán),并停止運(yùn)行,且直到線程再次進(jìn)入就緒狀態(tài),才有機(jī)會變?yōu)檫\(yùn)行狀態(tài)。阻塞的情況分三種:
    1. 等待阻塞:運(yùn)行狀態(tài)下的線程執(zhí)行wait()方法,JVM會把該線程放入等待池中(wait()方法會釋放線程持有的鎖)
    2. 同步阻塞:運(yùn)行狀態(tài)下的線程在獲取對象的同步鎖時(shí),若該同步鎖已被別的線程占用,則JVM會把該線程放入鎖池中
    3. 其他阻塞:運(yùn)行狀態(tài)下的線程執(zhí)行sleep()或join()方法,或者發(fā)出了輸入請求時(shí),JVM會把該線程置為阻塞狀態(tài)(sleep()方法不會釋放線程持有的鎖)
  • 死亡狀態(tài)(Dead):若線程的run()方法執(zhí)行完畢或者發(fā)生了異常退出,則該線程結(jié)束生命周期

線程的調(diào)度

  • 線程優(yōu)先級
    Java線程有優(yōu)先級,優(yōu)先級高的線程會獲得更多的運(yùn)行機(jī)會,但不保證一定能搶到CPU資源。
    Java線程的優(yōu)先級用整數(shù)表示,取值范圍是1~10。Thread類有以下三個(gè)靜態(tài)常量:static int MAX_PRIORITY; 線程可以具有的最高優(yōu)先級,取值為10static int MIN_PRIORITY; 線程可以具有的最低優(yōu)先級,取值為1
    static int NORM_PRIORITY; 分配給線程的默認(rèn)優(yōu)先級,取值為5
    可以通過Thread類的setPriority()/getPriority()方法設(shè)置/獲取線程的優(yōu)先級。每個(gè)線程都有默認(rèn)的優(yōu)先級。主線程的默認(rèn)優(yōu)先級為5。
    線程的優(yōu)先級有繼承關(guān)系,比如在線程A中創(chuàng)建了線程B,那么線程B將和線程A具有相同的優(yōu)先級。
    JVM提供了10個(gè)線程優(yōu)先級,但與常見的操作系統(tǒng)都不能很好的映射。如果希望程序能移植到各個(gè)操作系統(tǒng)中,應(yīng)該僅僅使用Thread類中三個(gè)靜態(tài)常量作為優(yōu)先級,這樣能保證同樣的優(yōu)先級在不同的操作系統(tǒng)中可以采用同樣的調(diào)度方式。
  • 線程睡眠
    Thread.sleep()方法可以使線程轉(zhuǎn)到阻塞狀態(tài),millis參數(shù)為線程睡眠的時(shí)間,以毫秒為單位。當(dāng)睡眠結(jié)束后,線程就會轉(zhuǎn)為就緒狀態(tài)。sleep()方法平臺移植性好。
  • 線程等待
    Object對象的wait()方法會導(dǎo)致當(dāng)前的線程等待,直到其他線程調(diào)用此對象的notify()方法或notifyAll()方法。這個(gè)兩個(gè)喚醒方法也是Object對象的方法,等價(jià)于調(diào)用wait(0)。
  • 線程讓步
    Thread.yield()方法可以將當(dāng)前正處于運(yùn)行狀態(tài)的線程置為就緒狀態(tài),把資源讓給相同或者更高優(yōu)先級的線程,但不保證其它線程一定能搶奪資源成功,也就是說,被yield()方法置為就緒狀態(tài)的線程有可能因?yàn)樵俅螕尩劫Y源而變?yōu)檫\(yùn)行狀態(tài)。
  • 線程加入
    Thread對象的join()方法可以選擇線程的執(zhí)行順序。如果在線程A中調(diào)用線程B的join()方法,則線程A轉(zhuǎn)入阻塞狀態(tài),直到線程B執(zhí)行結(jié)束,線程A才能由阻塞狀態(tài)轉(zhuǎn)為就緒狀態(tài)。
  • 線程喚醒
    Object對象的notify()方法可以喚醒在此對象監(jiān)視器上等待的單個(gè)線程。如果所有線程都在此對象上等待,則會任意選擇喚醒其中某一個(gè)線程。
    被喚醒的線程在搶奪、鎖定此對象方面沒有任何特權(quán)或劣勢,將以常規(guī)方式與在該對象上主動同步的其它所有線程進(jìn)行公平競爭。
    Object對象還有一個(gè)notifyAll()方法,可以用來喚醒在此對象監(jiān)視器上等待的所有線程。
  • 注意:Thread對象的suspend()方法和resume()方法因?yàn)橛兴梨i傾向,所以并不推薦使用。

常用函數(shù)說明

  1. Thread.sleep()

在指定的毫秒數(shù)內(nèi)讓當(dāng)前正在執(zhí)行的線程休眠(暫停執(zhí)行)。

  1. Thread對象的join()

當(dāng)前線程等待加入的線程終止后繼續(xù)運(yùn)行。
在很多情況下,主線程生成并起動了子線程,如果子線程里要進(jìn)行大量的耗時(shí)的運(yùn)算,主線程往往將于子線程之前結(jié)束,但是如果主線程需要用到子線程的處理結(jié)果,也就是主線程需要等待子線程執(zhí)行完成之后再結(jié)束,這個(gè)時(shí)候就要用到j(luò)oin()方法了。

  • 不加join()
package leif;

public class Test {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + "開始執(zhí)行");
        new Thread(new MyThread()).start();
        System.out.println(Thread.currentThread().getName() + "執(zhí)行完畢");
    }
}

class MyThread implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "開始執(zhí)行");

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + "執(zhí)行完畢");
    }
}
image.png
  • 加join()
package leif;

public class Test {
    public static void main(String[] args) {
        System.out.println(Thread.currentThread().getName() + "開始執(zhí)行");
        Thread thread = new Thread(new MyThread());
        thread.start();

        try {
            thread.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + "執(zhí)行完畢");
    }
}

class MyThread implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "開始執(zhí)行");

        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println(Thread.currentThread().getName() + "執(zhí)行完畢");
    }
}
image.png
  1. Thread.yield()

讓當(dāng)前運(yùn)行線程回到就緒狀態(tài),以允許其他線程獲得運(yùn)行機(jī)會。因此,使用yield()方法的目的是讓各線程之間能夠適當(dāng)?shù)妮嗈D(zhuǎn)執(zhí)行。但是,實(shí)際中無法保證yield()方法能夠達(dá)到讓步的目的,因?yàn)樽尣降木€程還有可能被再次選中。

  • sleep()方法和yield()方法的比較

sleep()方法使當(dāng)前線程進(jìn)入阻塞狀態(tài),所以執(zhí)行sleep()方法的線程在指定的時(shí)間內(nèi)肯定不會被執(zhí)行;yield()方法只是讓當(dāng)前線程重新回到就緒狀態(tài),所以執(zhí)行yield()方法的線程有可能在進(jìn)入到就緒狀態(tài)后馬上又被執(zhí)行。

  1. Thread對象的setPriority()/getPriority()

設(shè)置/獲取線程的優(yōu)先級,取值范圍是1~10。Thread類有以下三個(gè)靜態(tài)常量:static int MAX_PRIORITY; 線程可以具有的最高優(yōu)先級,取值為10static int MIN_PRIORITY; 線程可以具有的最低優(yōu)先級,取值為1
static int NORM_PRIORITY; 分配給線程的默認(rèn)優(yōu)先級,取值為5

  1. Object對象的wait()/notify()/notifyAll()

必須與synchronized(Object)一起使用,可以使當(dāng)前線程在Object的對象監(jiān)視器上等待/喚醒。

  • 實(shí)例

建立三個(gè)線程,A線程打印10次A,B線程打印10次B,C線程打印10次C,要求線程同時(shí)運(yùn)行,交替打印10次ABC。

package leif;

public class Test {
    public static void main(String[] args) {
        Object lockAB = new Object();
        Object lockBC = new Object();
        Object lockCA = new Object();
        new Thread(new MyThread(lockCA, lockAB), "A").start();
        new Thread(new MyThread(lockAB, lockBC), "B").start();
        new Thread(new MyThread(lockBC, lockCA), "C").start();
    }
}

class MyThread implements Runnable {
    private Object lock1;
    private Object lock2;

    public MyThread(Object lock1, Object lock2) {
        this.lock1 = lock1;
        this.lock2 = lock2;
    }

    @Override
    public void run() {
        String name = Thread.currentThread().getName();

        if ("B".equals(name)) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        } else if ("C".equals(name)) {
            try {
                Thread.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        for (int i = 1; i <= 10; i++) {
            synchronized (lock1) {
                synchronized (lock2) {
                    System.out.print(name);
                    lock2.notifyAll();
                }

                try {
                    lock1.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
image.png
  • sleep()方法和wait()方法的比較
  • 共同點(diǎn):
  1. 都是在多線程的環(huán)境下,都可以使線程阻塞指定的毫秒數(shù)
  2. 都可以通過interrupt()方法打斷線程的阻塞狀態(tài),從而使線程立刻拋出InterruptedException(但不建議使用該方法)。
    如果線程A希望立即結(jié)束線程B,則可以調(diào)用線程B的interrupt()方法。如果此時(shí)線程B正在wait/sleep/join,則線程B會立刻拋出InterruptedException,在catch(InterruptedException e) {}中直接return即可安全地結(jié)束線程。
    需要注意的是,InterruptedException是線程自己從內(nèi)部拋出的,并不是interrupt()方法拋出的。對某一線程調(diào)用interrupt()方法時(shí),如果該線程正在執(zhí)行普通的代碼,那么該線程根本就不會拋出InterruptedException。但是,一旦該線程處于阻塞狀態(tài)后,就會立刻拋出InterruptedException。
  • 不同點(diǎn):
sleep() wait()
Thread類的方法 Object對象的方法
保持同步鎖 釋放同步鎖
結(jié)束后線程進(jìn)入就緒狀態(tài) 結(jié)束后線程進(jìn)入鎖池等待
  1. Thread對象的isAlive()

判斷一個(gè)線程是否存活。

  1. Thread.currentThread()

獲取當(dāng)前線程。

  1. Thread對象的isDaemon()

判斷一個(gè)線程是否是守護(hù)線程。

  1. Thread對象的setDaemon()

設(shè)置一個(gè)線程為守護(hù)線程。

  1. Thread對象的setName()

設(shè)置線程的名字。

常見線程名詞解釋

  • 主線程:JVM調(diào)用程序的main()方法所產(chǎn)生的線程
  • 當(dāng)前線程:通過Thread.currentThread()方法獲取的線程
  • 后臺線程:指為其他線程提供服務(wù)的線程,也稱為守護(hù)線程。JVM的垃圾回收線程就是一個(gè)守護(hù)線程。用戶線程和守護(hù)線程的區(qū)別在于是否依賴于主線程的結(jié)束而結(jié)束,當(dāng)所有的用戶線程都結(jié)束后守護(hù)線程就會結(jié)束,無論此時(shí)守護(hù)線程是否執(zhí)行完畢
  • 前臺線程:是指接受后臺線程服務(wù)的線程,也成為用戶線程。由用戶線程創(chuàng)建的線程默認(rèn)也是用戶線程

線程同步

線程同步,指某一個(gè)時(shí)刻,只允許一個(gè)線程來訪問共享資源,線程同步其實(shí)是對對象加鎖,如果對象中的方法都是同步方法,那么某一時(shí)刻只能執(zhí)行一個(gè)方法。

  • 異步編程模型:線程A執(zhí)行線程A的,線程B執(zhí)行線程B的,兩個(gè)線程之間誰也不等誰
  • 同步編程模型:線程A和線程B先后執(zhí)行,線程B必須等線程A執(zhí)行結(jié)束之后才能執(zhí)行

總的來說:

  1. 線程同步的目的是為了保護(hù)多個(gè)線程同時(shí)訪問一個(gè)資源時(shí)對資源的破壞。
  2. 線程同步是通過鎖來實(shí)現(xiàn)的,每個(gè)對象都有且僅有一個(gè)鎖(對象監(jiān)視器),某個(gè)線程一旦獲取了該對象鎖,其他訪問該對象的線程就無法再訪問該對象鎖所鎖定的同步方法,只能等待該對象鎖被釋放后再訪問。
  3. 無論synchronized關(guān)鍵字加在方法上還是對象上,它取得的鎖都是對象,而不是把一段代碼或方法當(dāng)作鎖。
  4. 實(shí)現(xiàn)同步需要很大的系統(tǒng)開銷,甚至可能造成死鎖,所以應(yīng)盡量避免無謂的同步控制。
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容