黑馬程序員-深入并發(fā)系列學(xué)習(xí)-1-基礎(chǔ)夯實(shí)

并發(fā)編程對(duì)于很多人來說都是一個(gè)難點(diǎn),因?yàn)槠鋬?nèi)部涉及到的內(nèi)容非常繁雜骚勘,同時(shí)在實(shí)際開發(fā)中很多人雖然做了多年铐伴,但是仍然還停留在業(yè)務(wù)代碼開發(fā)。對(duì)于并發(fā)的接觸可以說是少之又少俏讹〉毖纾可是現(xiàn)在的面試中,并發(fā)這一部分又是一個(gè)高頻考點(diǎn)泽疆,很多人都在并發(fā)這座大山面前無法翻越户矢。

本系列會(huì)帶領(lǐng)大家從并發(fā)編程的基礎(chǔ)開始,逐步深入并發(fā)的核心殉疼。讓大家能夠系統(tǒng)的將并發(fā)全部知識(shí)點(diǎn)拿下逗嫡。

并發(fā)編程基礎(chǔ)

Java從誕生開始,其就已經(jīng)內(nèi)置了對(duì)于多線程的支持株依。當(dāng)多個(gè)線程能夠同時(shí)執(zhí)行時(shí)驱证,大多數(shù)情況下都能夠顯著提升系統(tǒng)性能,尤其現(xiàn)在的計(jì)算機(jī)普遍都是多核的恋腕,所以性能的提升會(huì)更加明顯抹锄。但是,多線程在使用中也需要注意諸多的問題荠藤,如果使用不當(dāng)伙单,也會(huì)對(duì)系統(tǒng)性能造成非常嚴(yán)重的影響。

進(jìn)程哈肖、線程吻育、協(xié)程

什么是進(jìn)程

進(jìn)程可以理解為就是應(yīng)用程序的啟動(dòng)實(shí)例。如微信淤井、Idea布疼、Navicat等摊趾,當(dāng)打開它們后,就相當(dāng)于開啟了一個(gè)進(jìn)程游两。每個(gè)進(jìn)程都會(huì)在操作系統(tǒng)中擁有獨(dú)立的內(nèi)存空間砾层、地址、文件資源贱案、數(shù)據(jù)資源等肛炮。進(jìn)程是資源分配和管理的最小單位。


image.png

1.1.2)什么是線程

線程從屬于進(jìn)程宝踪,是程序的實(shí)際執(zhí)行者侨糟,一個(gè)進(jìn)程中可以包含若干個(gè)線程,并且也可以把線程稱為輕量級(jí)進(jìn)程瘩燥。每個(gè)線程都會(huì)擁有自己的計(jì)數(shù)器秕重、堆棧、局部變量等屬性颤芬,并且能夠訪問共享的內(nèi)存變量悲幅。線程是操作系統(tǒng)(CPU)調(diào)度和執(zhí)行的最小單位。CPU會(huì)在這些線程上來回切換站蝠,讓使用者感覺線程是在同時(shí)執(zhí)行的汰具。


image.png

一個(gè)線程具有五種狀態(tài),分別為:新建菱魔、就緒留荔、運(yùn)行、阻塞澜倦、銷毀聚蝶。


image.png

同時(shí)對(duì)于線程狀態(tài)切換的工作,是由JVM中的TCB(Thread Control Block)來執(zhí)行藻治。

線程使用帶來的問題

有很多人都會(huì)存在一個(gè)誤區(qū)碘勉,在代碼中使用多線程,一定會(huì)為系統(tǒng)帶來性能提升桩卵,這個(gè)觀點(diǎn)是錯(cuò)誤的验靡。并發(fā)編程的目的是為了讓程序運(yùn)行的更快,但是雏节,絕對(duì)不是說啟動(dòng)的線程越多胜嗓,性能提升的就越大,其會(huì)受到很多因素的影響钩乍,如鎖問題辞州、線程狀態(tài)切換問題、線程上下文切換問題寥粹,還會(huì)受到硬件資源的影響变过,如CPU核數(shù)埃元。

什么叫做線程上下文切換

不管是在多核甚至單核處理器中,都是能夠以多線程形式執(zhí)行代碼的牵啦,CPU通過給每個(gè)線程分配CPU時(shí)間片來實(shí)現(xiàn)線程執(zhí)行間的快速切換亚情。 所謂的時(shí)間片就是CPU分配給每個(gè)線程的執(zhí)行時(shí)間妄痪,當(dāng)某個(gè)線程獲取到CPU時(shí)間片后哈雏,就會(huì)在一定時(shí)間內(nèi)執(zhí)行,當(dāng)時(shí)間片到期衫生,則該線程會(huì)進(jìn)入到掛起等待狀態(tài)裳瘪。時(shí)間片一般為幾十毫秒,通過在CPU的高速切換罪针,讓使用者感覺是在同時(shí)執(zhí)行彭羹。

同時(shí)還要保證線程在切換的過程中,要記錄線程被掛起時(shí)泪酱,已經(jīng)執(zhí)行了哪些指令滤淳、變量值是多少术健,那這點(diǎn)則是通過每個(gè)線程內(nèi)部的程序計(jì)數(shù)器來保證。

簡(jiǎn)單來說:線程從掛起到再加載的過程,就是一次上下文切換弯囊。其實(shí)比較耗費(fèi)資源的。


image.png

引起上下文切換的幾種情況:

  • 時(shí)間片用完台妆,CPU正常調(diào)度下一個(gè)任務(wù)础锐。
  • 被其他優(yōu)先級(jí)更高的任務(wù)搶占。
  • 執(zhí)行任務(wù)碰到IO阻塞勿锅,調(diào)度器掛起當(dāng)前任務(wù)帕膜,切換執(zhí)行下一個(gè)任務(wù)。
  • 用戶代碼主動(dòng)掛起當(dāng)前任務(wù)讓出CPU時(shí)間溢十。
  • 多任務(wù)搶占資源垮刹,由于沒有搶到被掛起。
  • 硬件中斷张弛。

生產(chǎn)者/消費(fèi)者模式實(shí)現(xiàn)


public class Producer extends Thread{

    private static final int QUEUE_SIZE=5;

    private final Queue queue;

    public Producer(Queue queue){
        super();
        this.queue=queue;
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {

            synchronized (queue){

                while (queue.size() >= QUEUE_SIZE){

                    System.out.println("隊(duì)列滿了荒典,等待消費(fèi)");
                    try {
                        queue.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                queue.add(i);
                System.out.println("入隊(duì)數(shù)據(jù):"+i);
                queue.notify();
            }
        }
    }
}
import java.util.Queue;

public class Consumer extends Thread{

    private final Queue queue;

    public Consumer(Queue queue){
        super();
        this.queue=queue;
    }

    @Override
    public void run() {
        while (true){

            synchronized (queue){

                while (queue.size() ==0){
                    System.out.println("隊(duì)列沒有數(shù)據(jù),等待生產(chǎn)");
                    try {
                        queue.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                //取出數(shù)據(jù)
                System.out.println("取出數(shù)據(jù):"+queue.poll());
                queue.notify();

            }
        }
    }
}
public class PCTest {

    public static void main(String[] args) {

        final Queue<Integer> queue = new LinkedList<>();
        Producer producer = new Producer(queue);
        Consumer consumer = new Consumer(queue);

        producer.start();
        consumer.start();
    }
}

演示效果如下:


image.png

這種方式雖然實(shí)現(xiàn)了生產(chǎn)者/消費(fèi)者模式乌庶,但其性能非常低下种蝶。因?yàn)樯婕暗搅送芥i、線程等待與喚醒的狀態(tài)轉(zhuǎn)換和線程上下文切換瞒大。這些操作都是機(jī)器耗費(fèi)性能的螃征。

什么是協(xié)程

協(xié)程是一種比線程更加輕量級(jí)的存在,一個(gè)線程中可以包含若干個(gè)協(xié)程透敌。同時(shí)Java本身是并不支持協(xié)程的盯滚。但是如python踢械、go都是支持的。協(xié)程不是被操作系統(tǒng)內(nèi)核所管理魄藕,而完全是由程序所控制(也就是在用戶態(tài)執(zhí)行)内列,所以其非常輕量級(jí)。而線程是由操作系統(tǒng)內(nèi)核管理背率,所以是重量級(jí)的话瞧。只要是由程序也就是用戶態(tài)完成操作,無需經(jīng)過操作系統(tǒng)內(nèi)核都是輕量級(jí)的寝姿。一旦經(jīng)過操作系統(tǒng)內(nèi)核就是重量級(jí)的交排。協(xié)程的開銷遠(yuǎn)遠(yuǎn)小于線程的開銷。

CPU時(shí)間片輪轉(zhuǎn)機(jī)制&優(yōu)化

之前已經(jīng)提到了線程的執(zhí)行饵筑,是依賴于CPU給每個(gè)線程分配的時(shí)間來進(jìn)行埃篓。在CPU時(shí)間片輪轉(zhuǎn)機(jī)制中,如果一個(gè)線程的時(shí)間片到期根资,則CPU會(huì)掛起該線程并給另一個(gè)線程分片一定的時(shí)間分片架专。如果進(jìn)程在時(shí)間片結(jié)束前阻塞或結(jié)束,則 CPU 會(huì)立即進(jìn)行切換玄帕。調(diào)度程序所要做的就是維護(hù)一張就緒進(jìn)程列表,當(dāng)進(jìn)程用完它的時(shí)間片后,它被移到隊(duì)列的末尾部脚。

時(shí)間片太短會(huì)導(dǎo)致頻繁的進(jìn)程切換,降低了 CPU 效率: 而太長(zhǎng)又可能引起對(duì)短的交互請(qǐng)求的響應(yīng)變差桨仿。將時(shí)間片設(shè)為 100ms 通常是一個(gè)比較合理的折衷睛低。

并行與并發(fā)的理解

對(duì)于這兩個(gè)概念,如果剛看到的話服傍,可能會(huì)很不屑钱雷。但是真的理解什么叫做并行,什么叫做并發(fā)嗎吹零?

所謂并發(fā)即讓多個(gè)任務(wù)能夠交替執(zhí)行罩抗,一般都會(huì)附帶一個(gè)時(shí)間單位,也就是所謂的在單位時(shí)間內(nèi)的并發(fā)量有多少灿椅。


image.png

所謂并行即讓多個(gè)任務(wù)能夠同時(shí)執(zhí)行套蒂。比如說:你可以一遍上廁所,一遍吃飯茫蛹。


image.png

線程啟動(dòng)&中止

線程的實(shí)現(xiàn)方式有兩種:繼承Thread類操刀、實(shí)現(xiàn)Runnable接口。但是有一些書籍或者文章會(huì)說有三種方式婴洼,即實(shí)現(xiàn)Callable接口骨坑。但通過該接口定義線程并不是Java標(biāo)準(zhǔn)的定義方式,而是基于Future思想來完成。 Java官方說明中欢唾,已經(jīng)明確指出且警,只有兩種方式。


image.png

那么Thread和Runnable有什么區(qū)別和聯(lián)系呢礁遣? 一般來說斑芜,Thread是對(duì)一個(gè)線程的抽象,而Runnable是對(duì)業(yè)務(wù)邏輯的抽象祟霍,并且Thread 可以接受任意一個(gè) Runnable 的實(shí)例并執(zhí)行杏头。

線程啟動(dòng)

/**
 * 新建線程
 */
public class NewThread {

    private static class UseThread extends Thread{
        @Override
        public void run() {

            System.out.println(Thread.currentThread().getName()+": use thread");
        }
    }

    private static class UseRunnable implements Runnable{

        @Override
        public void run() {

            System.out.println(Thread.currentThread().getName()+": use runnable");
        }
    }

    public static void main(String[] args) {

        System.out.println(Thread.currentThread().getName()+": use main");

        UseThread useThread = new UseThread();
        useThread.start();

        UseRunnable useRunnable = new UseRunnable();
        new Thread(useRunnable).start();


    }
}

優(yōu)化:?jiǎn)?dòng)線程前,最好為這個(gè)線程設(shè)置特定的線程名稱浅碾,這樣在出現(xiàn)問題時(shí)大州,給開發(fā)人員一些提示续语,快速定位到問題線程垂谢。

Thread.currentThread().setName("Runnable demo");

線程中止

線程在正常下當(dāng)run執(zhí)行完,或出現(xiàn)異常都會(huì)讓該線程中止疮茄。

理解suspend()滥朱、resume()、stop()

這三個(gè)方法對(duì)應(yīng)的是暫停力试、恢復(fù)和中止徙邻。對(duì)于這三個(gè)方法的使用效果演示如下:

public class Srs {

    private static class MyThread implements Runnable{
        @Override
        public void run() {
            Thread.currentThread().setName("my thread");
            DateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");

            while (true){

                System.out.println(Thread.currentThread().getName()+"run at"+dateFormat.format(new Date()));
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }


    public static void main(String[] args) throws InterruptedException {

        DateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");

        Thread thread = new Thread(new MyThread());

        //開啟線程
        System.out.println("開啟線程");
        thread.start();
        TimeUnit.SECONDS.sleep(3);

        //暫停線程
        System.out.println("暫停線程");
        thread.suspend();
        TimeUnit.SECONDS.sleep(3);

        //恢復(fù)線程
        System.out.println("恢復(fù)線程");
        thread.resume();
        TimeUnit.SECONDS.sleep(3);

        //中止線程
        System.out.println("中止線程");
        thread.stop();
    }
}

執(zhí)行結(jié)果

image.png

可以看到這三個(gè)方式,很好的完成了其本職工作畸裳。但是三個(gè)已經(jīng)在Java源碼中被標(biāo)注為過期方法缰犁。那這三個(gè)方式為什么會(huì)被標(biāo)記為過期方法呢?

當(dāng)調(diào)用suspend()時(shí)怖糊,線程不會(huì)將當(dāng)前持有的資源釋放(如鎖)帅容,而是占有者資源進(jìn)入到暫停狀態(tài),這樣的話伍伤,容易造成死鎖問題的出現(xiàn)并徘。

public class Srs {

    private static  Object obj = new Object();//作為一個(gè)鎖

    private static class MyThread implements Runnable{


        @Override
        public void run() {

            synchronized (obj){

                DateFormat dateFormat = new SimpleDateFormat("HH:mm:ss");

                while (true){
                    System.out.println(Thread.currentThread().getName()+"run at"+dateFormat.format(new Date()));

                    try {
                        TimeUnit.SECONDS.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }

        }
    }


    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(new MyThread(),"正常線程");
        Thread thread1 = new Thread(new MyThread(),"死鎖線程");

        //開啟線程
        thread.start();
        TimeUnit.SECONDS.sleep(3);

        //暫停線程
        thread.suspend();
        System.out.println("暫停線程");
        thread1.start();
        TimeUnit.SECONDS.sleep(3);

        //恢復(fù)線程
        /*thread.resume();
        System.out.println("恢復(fù)線程");
        TimeUnit.SECONDS.sleep(3);*/

        //中止線程
        /*thread.stop();
        System.out.println("中止線程");*/
    }
}

在上述代碼中,正常線程持有了鎖扰魂,當(dāng)調(diào)用suspend()時(shí)麦乞,因?yàn)樵摲椒ú粫?huì)釋放鎖,所以死鎖線程因?yàn)楂@取不到鎖而導(dǎo)致無法執(zhí)行劝评。所以該方法必須與resume()成對(duì)出現(xiàn)姐直。


image.png

當(dāng)調(diào)用stop()時(shí),會(huì)立即停止run()中剩余的操作蒋畜,包括在catch或finally語句中的內(nèi)容声畏。因此可能會(huì)導(dǎo)致一些收尾性的工作的得不到完成,如文件流百侧,數(shù)據(jù)庫(kù)等關(guān)閉砰识。并且會(huì)立即釋放該線程所持有的所有的鎖能扒,導(dǎo)致數(shù)據(jù)得不到同步的處理,出現(xiàn)數(shù)據(jù)不一致的問題辫狼。

public class StopProblem {

    public static void main(String[] args) throws Exception {

        TestObject testObject = new TestObject();

        Thread t1 = new Thread(() -> {
            try {
                testObject.print("1", "2");
            } catch (Exception e) {
                e.printStackTrace();
            }
        });

        t1.start();
        //讓子線程有執(zhí)行時(shí)間
        //Thread.sleep(1000);
        t1.stop();
        System.out.println("first : " + testObject.getFirst() + " " + "second : " + testObject.getSecond());
    }
}

class TestObject {

    private String first = "ja";
    private String second = "va";

    public synchronized void print(String first, String second) throws Exception {
        System.out.println(Thread.currentThread().getName());
        this.first = first;
        //模擬數(shù)據(jù)不一致
        //Thread.sleep(10000);
        this.second = second;
    }

    public String getFirst() {
        return first;
    }

    public String getSecond() {
        return second;
    }
}

線程中止的安全且優(yōu)雅姿勢(shì)

Java對(duì)于線程安全中止實(shí)現(xiàn)設(shè)計(jì)了一個(gè)中斷屬性初斑,其可以理解是線程的一個(gè)標(biāo)識(shí)位屬性。它用于表示一個(gè)運(yùn)行中的線程是否被其他線程進(jìn)行了中斷操作膨处。好比其他線程對(duì)這個(gè)線程打了一個(gè)招呼见秤,告訴它你該中斷了。通過interrupt()實(shí)現(xiàn)真椿。

public class InterruptDemo {


    private static class MyThread implements Runnable{

        @Override
        public void run() {

            while (true){

                System.out.println(Thread.currentThread().getName()+" is running");
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(new MyThread(),"myThread");

        thread.start();

        TimeUnit.SECONDS.sleep(3);

        thread.interrupt();
    }
}

添加該方法后鹃答,會(huì)出現(xiàn)一個(gè)異常,但是可以發(fā)現(xiàn)并不會(huì)線程的繼續(xù)執(zhí)行突硝。

線程通過通過檢查自身是否被中斷來進(jìn)行響應(yīng)测摔,可以通過isInterrupted()進(jìn)行判斷,如果返回值為true解恰,代表添加了中斷標(biāo)識(shí)锋八,返回false,代表沒有添加中斷標(biāo)識(shí)护盈。通過它可以對(duì)線程進(jìn)行中斷操作挟纱。

public class InterruptDemo {


    private static class MyThread implements Runnable{

        @Override
        public void run() {

            //while (true){
            while (!Thread.currentThread().isInterrupted()){

                System.out.println(Thread.currentThread().getName()+" is running");
            }

            System.out.println(Thread.currentThread().getName()+" Interrupt flag is : "+Thread.currentThread().isInterrupted());
        }
    }

    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(new MyThread(),"myThread");

        thread.start();

        TimeUnit.SECONDS.sleep(3);

        thread.interrupt();
    }
}

對(duì)線程中斷屬性的判斷,可以利用其進(jìn)行線程執(zhí)行的中斷操作腐宋。

線程也可以通過靜態(tài)方法Thread.interrupted()對(duì)中斷標(biāo)識(shí)進(jìn)行復(fù)位紊服,如果該線程已經(jīng)被添加了中斷標(biāo)識(shí),當(dāng)使用了該方法后胸竞,會(huì)將線程的中斷標(biāo)識(shí)由true改為false欺嗤。

public class InterruptDemo {


    private static class MyThread implements Runnable{

        @Override
        public void run() {

            //while (true){
            //while (!Thread.currentThread().isInterrupted()){
            while (!Thread.interrupted()){

                System.out.println(Thread.currentThread().getName()+" is running");
            }

            System.out.println(Thread.currentThread().getName()+" Interrupt flag is : "+Thread.currentThread().isInterrupted());
        }
    }

    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(new MyThread(),"myThread");

        thread.start();

        TimeUnit.SECONDS.sleep(3);

        thread.interrupt();
    }
}

中斷線程的注意點(diǎn)

線程在執(zhí)行的過程中,可能會(huì)進(jìn)行中斷或者阻塞撤师,比方說使用了sleep()或者wait()剂府。那么在線程執(zhí)行中一旦出現(xiàn)這類操作的話,則會(huì)出現(xiàn)InterruptedException異常剃盾。但是并不會(huì)影響線程的繼續(xù)執(zhí)行腺占。

那么一般都會(huì)進(jìn)行try/catch的操作。具體效果如下:

public class InterruptDemo {


    private static class MyThread implements Runnable{

        @Override
        public void run() {

            while (!Thread.interrupted()){

                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    System.out.println(Thread.currentThread().getName()+"in catch Interrupt flag is : "+Thread.currentThread().isInterrupted());
                }

                System.out.println(Thread.currentThread().getName()+" is running");
            }

            System.out.println(Thread.currentThread().getName()+" Interrupt flag is : "+Thread.currentThread().isInterrupted());
        }
    }

    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(new MyThread(),"myThread");

        thread.start();

        TimeUnit.SECONDS.sleep(3);

        thread.interrupt();
    }
}

此時(shí)可以明顯觀察到痒谴,線程會(huì)繼續(xù)向下執(zhí)行衰伯,并且catch中的中斷標(biāo)記屬性為false。因?yàn)門hread.interrupted()不僅會(huì)判斷線程中斷標(biāo)記屬性积蔚,同時(shí)如果該線程的中斷屬性為true意鲸,會(huì)將true改變?yōu)閒alse。

myThread is running
myThread is running
java.lang.InterruptedException: sleep interrupted
    at java.lang.Thread.sleep(Native Method)
    at java.lang.Thread.sleep(Thread.java:340)
    at java.util.concurrent.TimeUnit.sleep(TimeUnit.java:386)
    at base.InterruptDemo$MyThread.run(InterruptDemo.java:16)
    at java.lang.Thread.run(Thread.java:745)
myThread in catch Interrupt flag is : false
myThread is running
myThread is running
myThread is running
myThread is running

此時(shí)既然捕捉到了InterruptedException異常,代表該線程應(yīng)該被中斷了怎顾,應(yīng)該讓線程停止读慎,并且在catch中要將線程中斷掉。所以注意槐雾,在使用時(shí)夭委,應(yīng)該在catch中再次執(zhí)行interrupt(),讓線程中斷掉募强。 這樣可以在catch中在設(shè)置中斷標(biāo)記前株灸,進(jìn)行資源釋放。

public class InterruptDemo {


    private static class MyThread implements Runnable{

        @Override
        public void run() {
            //while (true){
            while (!Thread.currentThread().isInterrupted()){
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    System.out.println(Thread.currentThread().getName()+" in catch Interrupt flag is : "+Thread.currentThread().isInterrupted());
                    //todo:資源釋放
                    //設(shè)置中斷標(biāo)記
                    Thread.currentThread().interrupt();
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+" is running");
            }
            System.out.println(Thread.currentThread().getName()+" Interrupt flag is : "+Thread.currentThread().isInterrupted());
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new MyThread(),"myThread");
        thread.start();
        TimeUnit.SECONDS.sleep(3);
        thread.interrupt();
    }
}

同時(shí)要注意:處于死鎖下的線程擎值,無法被中斷

深入線程操作常見方法

理解run()&start()

這兩個(gè)方法都可以啟動(dòng)線程慌烧,但是它倆是有本質(zhì)上的區(qū)別的。當(dāng)線程執(zhí)行了 start()方法后鸠儿,才真正意義上的啟動(dòng)線程屹蚊,其會(huì)讓一個(gè)線程進(jìn)入就緒隊(duì)列等到分配CPU時(shí)間片,分到時(shí)間片后才會(huì)調(diào)用run()捆交。注意淑翼,同一個(gè)線程的start()不能被重復(fù)調(diào)用,否則會(huì)出現(xiàn)異常品追,因?yàn)橹貜?fù)調(diào)用了,start方法冯丙,線程的state就不是new了肉瓦,那么threadStatus就不等于0了。

//start源碼分析
public synchronized void start() {
    /**
        Java里面創(chuàng)建線程之后胃惜,必須要調(diào)用start方法才能創(chuàng)建一個(gè)線程泞莉,該方法會(huì)通過虛擬機(jī)啟動(dòng)一個(gè)本地線程,本地線程的創(chuàng)建會(huì)調(diào)用當(dāng)前系統(tǒng)去創(chuàng)建線程的方法進(jìn)行創(chuàng)建線程船殉。
        最終會(huì)調(diào)用run()將線程真正執(zhí)行起來
        0這個(gè)狀態(tài)鲫趁,等于‘New’這個(gè)狀態(tài)。
         */
    if (threadStatus != 0)
        throw new IllegalThreadStateException();

    /* 線程會(huì)加入到線程分組利虫,然后執(zhí)行start0() */
    group.add(this);

    boolean started = false;
    try {
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
                  it will be passed up the call stack */
        }
    }
}

執(zhí)行流程圖

image.png

而run()則僅僅是一個(gè)普通方法挨厚,與類中的成員方法意義相同。在該方法中可以實(shí)現(xiàn)線程執(zhí)行的業(yè)務(wù)邏輯糠惫。但并不會(huì)以異步的方式將線程啟動(dòng)疫剃,換句話說就是并不會(huì)去開啟一個(gè)新的線程。其可以單獨(dú)執(zhí)行硼讽,也可以重復(fù)執(zhí)行巢价。

理解yield()

當(dāng)某個(gè)線程調(diào)用了這個(gè)方法后,該線程立即釋放自己持有的時(shí)間片。線程會(huì)進(jìn)入到就緒狀態(tài)壤躲,同時(shí)CPU會(huì)重新選擇一個(gè)線程賦予時(shí)間分片城菊,但注意,調(diào)用了這個(gè)方法的線程碉克,也有可能被CPU再次選中賦予執(zhí)行役电。

而且該方法不會(huì)釋放鎖。 如需釋放鎖的話棉胀,可以在調(diào)用該方法前自己手動(dòng)釋放法瑟。

public class Demo {

    private static class MyThread extends Thread {

        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {

                System.out.println(Thread.currentThread().getName() + " " + i);

                if (i==5){
                    System.out.println(Thread.currentThread().getName());
                    yield();
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {

        Thread thread1 = new Thread(new MyThread());
        Thread thread2 = new Thread(new MyThread());
        Thread thread3 = new Thread(new MyThread());

        thread1.start();
        thread2.start();
        thread3.start();
    }
}

從結(jié)果看出,當(dāng)調(diào)用了該方法后線程會(huì)讓出自己的時(shí)間分片唁奢,但也有可能被再次選中執(zhí)行霎挟。


image.png

理解join()

該方法的使用,在實(shí)際開發(fā)中麻掸,應(yīng)用的是比較少的酥夭。但在面試中,常常伴隨著產(chǎn)生一個(gè)問題脊奋,如何保證線程的執(zhí)行順序熬北? 就可以通過該方法來設(shè)置。

使用

當(dāng)線程調(diào)用了該方法后诚隙,線程狀態(tài)會(huì)從就緒狀態(tài)進(jìn)入到運(yùn)行狀態(tài)讶隐。

public class JoinDemo {

    private static class MyThread extends Thread{

        int i;
        Thread previousThread; //上一個(gè)線程

        public MyThread(Thread previousThread,int i){
            this.previousThread=previousThread;
            this.i=i;
        }

        @Override
        public void run() {
            //調(diào)用上一個(gè)線程的join方法. 不使用join方法解決是不確定的
            //previousThread.join();
            System.out.println("num:"+i);
        }
    }

    public static void main(String[] args) {

        Thread previousThread=Thread.currentThread();

        for(int i=0;i<10;i++){
            //每一個(gè)線程實(shí)現(xiàn)都持有前一個(gè)線程的引用。
            MyThread joinDemo=new MyThread(previousThread,i);
            joinDemo.start();
            previousThread=joinDemo;
        }
    }
}
image.png

可是等到開啟了join之后久又,結(jié)果就是有序的了巫延。


image.png

根據(jù)結(jié)果可以看到,當(dāng)前線程需要等待previousThread線程終止之后才從thread.join返回地消÷澹可以理解完,線程會(huì)在join處等待脉执。

原理剖析

//源碼解析
public final void join() throws InterruptedException {
    join(0);
}
...
    public final synchronized void join(long millis) throws InterruptedException {
    long base = System.currentTimeMillis();
    long now = 0;
    if (millis < 0) {
        throw new IllegalArgumentException("timeout value is negative");
    }
    //判斷是否攜帶阻塞的超時(shí)時(shí)間疼阔,等于0表示沒有設(shè)置超時(shí)時(shí)間
    if (millis == 0) { 
        //isAlive獲取線程狀態(tài),無限等待直到previousThread線程結(jié)束
        while (isAlive()) {
            //調(diào)用Object中的wait方法實(shí)現(xiàn)線程的阻塞
            wait(0); 
        }
    } else { //阻塞直到超時(shí)
        while (isAlive()) { 
            long delay = millis - now;
            if (delay <= 0) {
                break;
            }
            wait(delay);
            now = System.currentTimeMillis() - base;
        }
    }
}

可以看到剛方法是被synchronized修飾的半夷,因?yàn)樵谄鋬?nèi)部對(duì)于線程阻塞的實(shí)現(xiàn)婆廊,是通過Object中wait方法實(shí)現(xiàn)的,而要調(diào)用wait()玻熙,則必須添加synchronized否彩。

為什么join()阻塞的是主線程呢?按照上述的理解嗦随,很多人認(rèn)為其應(yīng)該阻塞的是previousThread列荔。實(shí)際上主線程會(huì)持有previousThread這個(gè)對(duì)象的鎖敬尺,然后調(diào)用wait方法去阻塞,而這個(gè)方法的調(diào)用者是在主線程中的贴浙。所以造成主線程阻塞砂吞。

為什么previousThread線程執(zhí)行完畢就能夠喚醒住線程呢?或者說是在什么時(shí)候喚醒的崎溃?對(duì)于wait的使用蜻直,對(duì)應(yīng)的會(huì)有notify或notifyAll。在JVM內(nèi)部袁串,會(huì)設(shè)置native線程對(duì)象為null同時(shí)調(diào)用notifyAll喚醒所有線程概而。

總的來說,Thread.join其實(shí)底層是通過wait/notifyall來實(shí)現(xiàn)線程的通信達(dá)到線程阻塞的目的囱修;當(dāng)線程執(zhí)行結(jié)束以后赎瑰,會(huì)觸發(fā)兩個(gè)事情,第一個(gè)是設(shè)置native線程對(duì)象為null破镰、第二個(gè)是通過notifyall方法餐曼,讓等待在previousThread對(duì)象鎖上的wait方法被喚醒。

線程優(yōu)先級(jí)

操作對(duì)于線程執(zhí)行鲜漩,是通過CPU時(shí)間片來調(diào)用運(yùn)行的源譬。那么一個(gè)線程被分配的時(shí)間片的多少,就決定了其使用資源的多少孕似。而線程優(yōu)先級(jí)就是決定線程需要能夠使用資源多少的線程屬性踩娘。

線程優(yōu)先級(jí)的范圍是1~10。一個(gè)線程的默認(rèn)優(yōu)先級(jí)是5鳞青,可以在構(gòu)建線程時(shí)霸饲,通過setPriority()修改該線程的優(yōu)先級(jí)。優(yōu)先級(jí)高的線程分配時(shí)間片的數(shù)量會(huì)高于優(yōu)先級(jí)低的線程臂拓。

一般來說對(duì)于頻繁阻塞的線程需要設(shè)置優(yōu)先級(jí)高點(diǎn),而偏重計(jì)算的線程優(yōu)先級(jí)會(huì)設(shè)置低些习寸,確保處理器不會(huì)被獨(dú)占胶惰。

但注意,線程優(yōu)先級(jí)不能作為線程執(zhí)行正確性的依賴霞溪,因?yàn)椴煌牟僮飨到y(tǒng)可能會(huì)忽略優(yōu)先級(jí)的設(shè)置孵滞。

守護(hù)線程

守護(hù)線程是一種支持形的線程,我們之前創(chuàng)建的線程都可以稱之為用戶線程鸯匹。通過守護(hù)線程可以完成一些支持性的工作坊饶,如GC、分布式鎖續(xù)期殴蓬。守護(hù)線程會(huì)伴隨著用戶線程的結(jié)束而結(jié)束匿级。

對(duì)于守護(hù)線程的創(chuàng)建蟋滴,可以通過setDaemon()設(shè)置。

public class DaemonDemo {

    private static class MyThread implements Runnable{

        @Override
        public void run() {

            while (!Thread.currentThread().isInterrupted()){

                try {
                    System.out.println(Thread.currentThread().getName());
                } finally {
                    System.out.println("thread run into finally");
                }
            }
        }
    }


    public static void main(String[] args) throws InterruptedException {

        Thread thread = new Thread(new MyThread());
        //設(shè)置守護(hù)線程
        thread.setDaemon(true);
        thread.start();
        TimeUnit.SECONDS.sleep(5);
    }
}

當(dāng)線程實(shí)例沒有被設(shè)置為守護(hù)線程時(shí)痘绎,該線程并不會(huì)隨著主線程的結(jié)束而結(jié)束津函。但是當(dāng)被設(shè)置為守護(hù)線程后,當(dāng)主線程結(jié)束孤页,該線程也會(huì)伴隨著結(jié)束尔苦。同時(shí)守護(hù)線程不一定會(huì)執(zhí)行finally代碼塊。所以當(dāng)線程被設(shè)定為守護(hù)線程后行施,無法確保清理資源等操作一定會(huì)被執(zhí)行允坚。

線程狀態(tài)

理解了上述方法后,再來看一下這些對(duì)于線程狀態(tài)轉(zhuǎn)換能起到什么樣的影響蛾号。


image.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
禁止轉(zhuǎn)載稠项,如需轉(zhuǎn)載請(qǐng)通過簡(jiǎn)信或評(píng)論聯(lián)系作者。
  • 序言:七十年代末须教,一起剝皮案震驚了整個(gè)濱河市皿渗,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌轻腺,老刑警劉巖乐疆,帶你破解...
    沈念sama閱讀 218,607評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異贬养,居然都是意外死亡挤土,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,239評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門误算,熙熙樓的掌柜王于貴愁眉苦臉地迎上來新荤,“玉大人,你說我怎么就攤上這事盟戏「匮” “怎么了?”我有些...
    開封第一講書人閱讀 164,960評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵蚊夫,是天一觀的道長(zhǎng)诉字。 經(jīng)常有香客問我,道長(zhǎng)知纷,這世上最難降的妖魔是什么壤圃? 我笑而不...
    開封第一講書人閱讀 58,750評(píng)論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮琅轧,結(jié)果婚禮上伍绳,老公的妹妹穿的比我還像新娘。我一直安慰自己乍桂,他們只是感情好冲杀,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,764評(píng)論 6 392
  • 文/花漫 我一把揭開白布效床。 她就那樣靜靜地躺著,像睡著了一般漠趁。 火紅的嫁衣襯著肌膚如雪扁凛。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,604評(píng)論 1 305
  • 那天闯传,我揣著相機(jī)與錄音谨朝,去河邊找鬼。 笑死甥绿,一個(gè)胖子當(dāng)著我的面吹牛字币,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播共缕,決...
    沈念sama閱讀 40,347評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼洗出,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來了图谷?” 一聲冷哼從身側(cè)響起翩活,我...
    開封第一講書人閱讀 39,253評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎便贵,沒想到半個(gè)月后菠镇,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,702評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡承璃,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,893評(píng)論 3 336
  • 正文 我和宋清朗相戀三年利耍,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片盔粹。...
    茶點(diǎn)故事閱讀 40,015評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡隘梨,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出舷嗡,到底是詐尸還是另有隱情轴猎,我是刑警寧澤,帶...
    沈念sama閱讀 35,734評(píng)論 5 346
  • 正文 年R本政府宣布进萄,位于F島的核電站税稼,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏垮斯。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,352評(píng)論 3 330
  • 文/蒙蒙 一只祠、第九天 我趴在偏房一處隱蔽的房頂上張望兜蠕。 院中可真熱鬧,春花似錦抛寝、人聲如沸熊杨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,934評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽晶府。三九已至桂躏,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間川陆,已是汗流浹背剂习。 一陣腳步聲響...
    開封第一講書人閱讀 33,052評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留较沪,地道東北人鳞绕。 一個(gè)月前我還...
    沈念sama閱讀 48,216評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像尸曼,于是被迫代替她去往敵國(guó)和親们何。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,969評(píng)論 2 355

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