多線程基礎體系知識清單

前言

本文會介紹Java中多線程與并發的基礎,適合初學者食用。

線程與進程的區別

在計算機發展初期,每臺計算機是串行地執行任務的,如果碰上需要IO的地方,還需要等待長時間的用戶IO,后來經過一段時間有了批處理計算機,其可以批量串行地處理用戶指令,但本質還是串行,還是不能并發執行。

如何解決并發執行的問題呢?于是引入了進程的概念,每個進程獨占一份內存空間,進程是內存分配的最小單位,相互間運行互不干擾且可以相互切換,現在我們所看到的多個進程“同時"在運行,實際上是進程高速切換的效果。

那么有了線程之后,我們的計算機系統看似已經很完美了,為什么還要進入線程呢?如果一個進程有多個子任務,往往一個進程需要逐個去執行這些子任務,但往往這些子任務是不相互依賴的,可以并發執行,所以需要CPU進行更細粒度的切換。所以就引入了線程的概念,線程隸屬于某一個進程,它共享進程的內存資源,相互間切換更快速。

進程與線程的區別:

  1. 進程是資源分配的最小單位,線程是CPU調度的最小單位。所有與進程相關的資源,均被記錄在PCB中。

  2. 線程隸屬于某一個進程,共享所屬進程的資源。線程只由堆棧寄存器、程序計數器和TCB構成。

  3. 進程可以看作獨立的應用,線程不能看作獨立的應用。

  4. 進程有獨立的地址空間,相互不影響,而線程只是進程的不同執行路徑,如果線程掛了,進程也就掛了。所以多進程的程序比多線程程序健壯,但是切換消耗資源多。

Java中進程與線程的關系:

  1. 運行一個程序會產生一個進程,進程至少包含一個線程。

  2. 每個進程對應一個JVM實例,多個線程共享JVM中的堆。

  3. Java采用單線程編程模型,程序會自動創建主線程 。

  4. 主線程可以創建子線程,原則上要后于子線程完成執行。


線程的start方法和run方法的區別

區別

Java中創建線程的方式有兩種,不管使用繼承Thread的方式還是實現Runnable接口的方式,都需要重寫run方法。調用start方法會創建一個新的線程并啟動,run方法只是啟動線程后的回調函數,如果調用run方法,那么執行run方法的線程不會是新創建的線程,而如果使用start方法,那么執行run方法的線程就是我們剛剛啟動的那個線程。

程序驗證

public class Main {
    public static void main(String[] args) {
        Thread thread = new Thread(new SubThread());
        thread.run();
        thread.start();
    }

}
class SubThread implements Runnable{

    @Override
    public void run() {
        // TODO Auto-generated method stub
        System.out.println("執行本方法的線程:"+Thread.currentThread().getName());
    }

}

 

 

多線程基礎體系知識清單


Thread和Runnable的關系

Thread源碼

多線程基礎體系知識清單

Runnable源碼

多線程基礎體系知識清單

區別

通過上述源碼圖,不難看出,Thread是一個類,而Runnable是一個接口,Runnable接口中只有一個沒有實現的run方法,可以得知,Runnable并不能獨立開啟一個線程,而是依賴Thread類去創建線程,執行自己的run方法,去執行相應的業務邏輯,才能讓這個類具備多線程的特性。

使用繼承Thread方式和實現Runable接口方式分別創建子線程

使用繼承Thread類方式創建子線程

public class Main extends Thread{
    public static void main(String[] args) {
        Main main = new Main();
        main.start();
    }
    @Override
    public void run() {
        System.out.println("通過繼承Thread接口方式創建子線程成功,當前線程名:"+Thread.currentThread().getName());
    }

}

 

運行結果:

多線程基礎體系知識清單

使用實現Runnable接口方式創建子線程

public class Main{
    public static void main(String[] args) {
        SubThread subThread = new SubThread();
        Thread thread = new Thread(subThread);
        thread.start();
    }

}
class SubThread implements Runnable{

    @Override
    public void run() {
        // TODO Auto-generated method stub
        System.out.println("通過實現Runnable接口創建子線程成功,當前線程名:"+Thread.currentThread().getName());
    }

}

 

運行結果:

多線程基礎體系知識清單

使用匿名內部類方式創建子線程

public class Main{
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                // TODO Auto-generated method stub
                System.out.println("使用匿名內部類方式創建線程成功,當前線程名:"+Thread.currentThread().getName());
            }
        });
        thread.start();
    }
}

 

運行結果:

多線程基礎體系知識清單

關系

  1. Thread是實現了Runnable接口的類,使得run支持多線程。

  2. 因類的單一繼承原則,推薦使用Runnable接口,可以使程序更加靈活。


如何實現處理多線程的返回值

通過剛才的學習,我們知道多線程的邏輯需要放到run方法中去執行,而run方法是沒有返回值的,那么遇到需要返回值的狀況就不好解決,那么如何實現子線程返回值呢?

主線程等待法

通過讓主線程等待,直到子線程運行完畢為止。

實現方式:

public class Main{
    static String str;
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                str="子線程執行完畢";
            }
        });
        thread.start();
        //如果子線程還未對str進行賦值,則一直輪轉
        while(str==null) {}
        System.out.println(str);
    }
}

 

使用Thread中的join()方法

join()方法可以阻塞當前線程以等待子線程處理完畢。

實現方式:

public class Main{
    static String str;
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                str="子線程執行完畢";
            }
        });
        thread.start();
        //如果子線程還未對str進行賦值,則一直輪轉
        try {
            thread.join();
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println(str);
    }
}

 

join方法能做到比主線程等待法更精準的控制,但是join方法的控制粒度并不夠細。比如,我需要控制子線程將字符串賦一個特定的值時,再執行主線程,這種操作join方法是沒有辦法做到的。

通過Callable接口實現:通過FutureTask或者線程池獲取

在JDK1.5之前,線程是沒有返回值的,通常程序猿需要獲取子線程返回值頗費周折,現在Java有了自己的返回值線程,即實現了Callable接口的線程,執行了實現Callable接口的線程之后,可以獲得一個Future對象,在該對象上調用一個get方法,就可以執行子線程的邏輯并獲取返回的Object。

實現方式1(錯誤示例):

public class Main implements Callable<String>{

    @Override
    public String call() throws Exception {
        // TODO Auto-generated method stub
        String str = "我是帶返回值的子線程";
        return str;
    }
    public static void main(String[] args) {
        Main main = new Main();
        try {
            String str = main.call();
            System.out.println(str);
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

 

運行結果:

多線程基礎體系知識清單

實現方式2(使用FutureTask):

public class Main implements Callable<String>{

    @Override
    public String call() throws Exception {
        // TODO Auto-generated method stub
        String str = "我是帶返回值的子線程";
        return str;
    }
    public static void main(String[] args) {
        FutureTask<String> task = new FutureTask<String>(new Main());
        new Thread(task).start();
        try {
            if(!task.isDone()) {
                System.out.println("任務沒有執行完成");
            }
            System.out.println("等待中...");
            Thread.sleep(3000);
            System.out.println(task.get());

        } catch (InterruptedException | ExecutionException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}

 

運行結果:

多線程基礎體系知識清單

實現方法3(使用線程池配合Future獲取):

public class Main implements Callable<String>{

    @Override
    public String call() throws Exception {
        // TODO Auto-generated method stub
        String str = "我是帶返回值的子線程";
        return str;
    }
    public static void main(String[] args) throws InterruptedException, ExecutionException {
        ExecutorService newCacheThreadPool = Executors.newCachedThreadPool(); 
        Future<String> future = newCacheThreadPool.submit(new Main());
        if(!future.isDone()) {
            System.out.println("線程尚未執行結束");
        }
        System.out.println("等待中");
        Thread.sleep(300);
        System.out.println(future.get());
        newCacheThreadPool.shutdown();
    }
}

 

運行結果:

多線程基礎體系知識清單


線程的狀態

Java線程主要分為以下六個狀態:新建態(new),運行態(Runnable),無限期等待(Waiting),限期等待(TimeWaiting),阻塞態(Blocked),結束(Terminated)

新建(new)

新建態是線程處于已被創建但沒有被啟動的狀態,在該狀態下的線程只是被創建出來了,但并沒有開始執行其內部邏輯。

運行(Runnable)

運行態分為Ready和Running,當線程調用start方法后,并不會立即執行,而是去爭奪CPU,當線程沒有開始執行時,其狀態就是Ready,而當線程獲取CPU時間片后,從Ready態轉為Running態。

等待(Waiting)

處于等待狀態的線程不會自動蘇醒,而只有等待被其它線程喚醒,在等待狀態中該線程不會被CPU分配時間,將一直被阻塞。以下操作會造成線程的等待:

  1. 沒有設置timeout參數的Object.wait()方法。

  2. 沒有設置timeout參數的Thread.join()方法。

  3. LockSupport.park()方法(實際上park方法并不是LockSupport提供的,而是在Unsafe中,LockSupport只是對其做了一層封裝,可以看我的另一篇博客《鎖》,里面對于ReentrantLock的源碼解析有提到這個方法)。

 

鎖:https://juejin.im/post/5d8da403f265da5b5d203bf4

限期等待(TimeWaiting)

處于限期等待的線程,CPU同樣不會分配時間片,但存在于限期等待的線程無需被其它線程顯式喚醒,而是在等待時間結束后,系統自動喚醒。以下操作會造成線程限時等待:

  1. Thread.sleep()方法。

  2. 設置了timeout參數的Object.wait()方法。

  3. 設置了timeout參數的Thread.join()方法。

  4. LockSupport.parkNanos()方法。

  5. LockSupport.parkUntil()方法。

阻塞(Blocked)

當多個線程進入同一塊共享區域時,例如Synchronized塊、ReentrantLock控制的區域等,會去整奪鎖,成功獲取鎖的線程繼續往下執行,而沒有獲取鎖的線程將進入阻塞狀態,等待獲取鎖。

結束(Terminated)

已終止線程的線程狀態,線程已結束執行。


Sleep和Wait的區別

Sleep和Wait者兩個方法都可以使線程進入限期等待的狀態,那么這兩個方法有什么區別呢?

  1. sleep方法由Thread提供,而wait方法由Object提供。

  2. sleep方法可以在任何地方使用,而wait方法只能在synchronized塊或synchronized方法中使用(因為必須獲wait方法會釋放鎖,只有獲取鎖了才能釋放鎖)。

  3. sleep方法只會讓出CPU,不會釋放鎖,而wait方法不僅會讓出CPU,還會釋放鎖。

測試代碼:

public class Main{
    public static void main(String[] args) {
        Thread threadA = new Thread(new ThreadA());
        Thread threadB = new Thread(new ThreadB());

        threadA.setName("threadA");
        threadB.setName("threadB");

        threadA.start();
        threadB.start();
    }

    public static synchronized void print() {
        System.out.println("當前線程:"+Thread.currentThread().getName()+"執行Sleep");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println("當前線程:"+Thread.currentThread().getName()+"執行Wait");
        try {
            Main.class.wait(1000);
        } catch (InterruptedException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
        System.out.println("當前線程:"+Thread.currentThread().getName()+"執行完畢");
    }
}
class ThreadA implements Runnable{
    @Override
    public void run() {
        // TODO Auto-generated method stub
        Main.print();
    }

}
class ThreadB implements Runnable{
    @Override
    public void run() {
        // TODO Auto-generated method stub
        Main.print();
    }

}

 

執行結果:

多線程基礎體系知識清單

從上面的結果可以分析出:當線程A執行sleep后,等待一秒被喚醒后繼續持有鎖,執行之后的代碼,而執行wait之后,立即釋放了鎖,不僅讓出了CPU還讓出了鎖,而后線程B立即持有鎖開始執行,和線程A執行了同樣的步驟,當線程B執行wait方法之后,釋放鎖,然后線程A拿到鎖打印了第一個執行完畢,然后線程B打印執行完畢。


notify和notifyAll的區別

notify

notify可以喚醒一個處于等待狀態的線程,上代碼:

public class Main{
    public static void main(String[] args) {
        Object lock = new Object();
        Thread threadA = new Thread(new Runnable() {

            @Override
            public void run() {
                synchronized (lock) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    print();

                }
            }
        });
        Thread threadB = new Thread(new Runnable() {

            @Override
            public void run() {
                synchronized (lock) {
                    print();
                    lock.notify();
                }

            }
        });

        threadA.setName("threadA");
        threadB.setName("threadB");

        threadA.start();
        threadB.start();
    }

    public static void print() {
            System.out.println("當前線程:"+Thread.currentThread().getName()+"執行print");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.out.println("當前線程:"+Thread.currentThread().getName()+"執行完畢");

    }
}

 

執行結果:

多線程基礎體系知識清單

代碼解釋:線程A在開始執行時立即調用wait進入無限等待狀態,如果沒有別的線程來喚醒它,它將一直等待下去,所以此時B持有鎖開始執行,并且在執行完畢時調用了notify方法,該方法可以喚醒wait狀態的A線程,于是A線程蘇醒,開始執行剩下的代碼。

notifyAll

notifyAll可以用于喚醒所有等待的線程,使所有處于等待狀態的線程都變為ready狀態,去重新爭奪鎖。

public class Main{
    public static void main(String[] args) {
        Object lock = new Object();
        Thread threadA = new Thread(new Runnable() {

            @Override
            public void run() {
                synchronized (lock) {
                    try {
                        lock.wait();
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                    print();

                }
            }
        });
        Thread threadB = new Thread(new Runnable() {

            @Override
            public void run() {
                synchronized (lock) {
                    print();
                    lock.notifyAll();
                }

            }
        });

        threadA.setName("threadA");
        threadB.setName("threadB");

        threadA.start();
        threadB.start();
    }

    public static void print() {
            System.out.println("當前線程:"+Thread.currentThread().getName()+"執行print");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                // TODO Auto-generated catch block
                e.printStackTrace();
            }
            System.out.println("當前線程:"+Thread.currentThread().getName()+"執行完畢");

    }
}

 

執行結果:

多線程基礎體系知識清單

要喚醒前一個例子中的線程A,不光notify方法可以做到,調用notifyAll方法同樣也可以做到,那么兩者有什么區別呢?

區別

要說清楚他們的區別,首先要簡單的說一下Java synchronized的一些原理,在openjdk中查看java的源碼可以看到,java對象中存在monitor鎖,monitor對象中包含鎖池和等待池。

鎖池,假設有多個對象進入synchronized塊爭奪鎖,而此時已經有一個對象獲取到了鎖,那么剩余爭奪鎖的對象將直接進入鎖池中。

等待池,假設某個線程調用了對象的wait方法,那么這個線程將直接進入等待池,而等待池中的對象不會去爭奪鎖,而是等待被喚醒。

下面可以說notify和notifyAll的區別了:

notifyAll會讓所有處于等待池中的線程全部進入鎖池去爭奪鎖,而notify只會隨機讓其中一個線程去爭奪鎖。


yield方法

概念

/**
     * A hint to the scheduler that the current thread is willing to yield
     * its current use of a processor. The scheduler is free to ignore this
     * hint.
     *
     * <p> Yield is a heuristic attempt to improve relative progression
     * between threads that would otherwise over-utilise a CPU. Its use
     * should be combined with detailed profiling and benchmarking to
     * ensure that it actually has the desired effect.
     *
     * <p> It is rarely appropriate to use this method. It may be useful
     * for debugging or testing purposes, where it may help to reproduce
     * bugs due to race conditions. It may also be useful when designing
     * concurrency control constructs such as the ones in the
     * {@link java.util.concurrent.locks} package.
     */
    public static native void yield();

 

yield源碼上有一段長長的注釋,其大意是說:當前線程調用yield方法時,會給當前線程調度器一個暗示,當前線程愿意讓出CPU的使用,但是它的作用應結合詳細的分析和測試來確保已經達到了預期的效果,因為調度器可能會無視這個暗示,使用這個方法是不那么合適的,或許在測試環境中使用它會比較好。

測試:

public class Main{
    public static void main(String[] args) {
        Thread threadA = new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println("ThreadA正在執行yield");
                Thread.yield();
                System.out.println("ThreadA執行yield方法完成");
            }
        });
        Thread threadB = new Thread(new Runnable() {

            @Override
            public void run() {
                System.out.println("ThreadB正在執行yield");
                Thread.yield();
                System.out.println("ThreadB執行yield方法完成");

            }
        });

        threadA.setName("threadA");
        threadB.setName("threadB");

        threadA.start();
        threadB.start();
    }

 

測試結果:

多線程基礎體系知識清單

多線程基礎體系知識清單

可以看出,存在不同的測試結果,這里選出兩張。

第一種結果:線程A執行完yield方法,讓出cpu給線程B執行。然后兩個線程繼續執行剩下的代碼。

第二種結果:線程A執行yield方法,讓出cpu給線程B執行,但是線程B執行yield方法后并沒有讓出cpu,而是繼續往下執行,此時就是系統無視了這個暗示。


interrupt方法

中止線程

interrupt函數可以中斷一個線程,在interrupt之前,通常使用stop方法來終止一個線程,但是stop方法過于暴力,它的特點是,不論被中斷的線程之前處于一個什么樣的狀態,都無條件中斷,這會導致被中斷的線程后續的一些清理工作無法順利完成,引發一些不必要的異常和隱患,還有可能引發數據不同步的問題。

溫柔的interrupt方法

interrupt方法的原理與stop方法相比就顯得溫柔的多,當調用interrupt方法去終止一個線程時,它并不會暴力地強制終止線程,而是通知這個線程應該要被中斷了,和yield一樣,這也是一種暗示,至于是否應該中斷,由被中斷的線程自己去決定。當對一個線程調用interrupt方法時:

  1. 如果該線程處于被阻塞狀態,則立即退出阻塞狀態,拋出InterruptedException異常。

  2. 如果該線程處于running狀態,則將該線程的中斷標志位設置為true,被設置的線程繼續運行,不受影響,當運行結束時由線程決定是否被中斷。


線程池

線程池的引入是用來解決在日常開發的多線程開發中,如果開發者需要使用到非常多的線程,那么這些線程在被頻繁的創建和銷毀時,會對系統造成一定的影響,有可能系統在創建和銷毀這些線程所耗費的時間會比完成實際需求的時間還要長。

另外,在線程很多的狀況下,對線程的管理就形成了一個很大的問題,開發者通常要將注意力從功能上轉移到對雜亂無章的線程進行管理上,這項動作實際上是非常耗費精力的。

利用Executors創建不同的線程池滿足不同場景的需求

newFixThreadPool(int nThreads)
指定工作線程數量的線程池。

newCachedThreadPool()
處理大量中斷事件工作任務的線程池,

  1. 試圖緩存線程并重用,當無緩存線程可用時,就會創建新的工作線程。

  2. 如果線程閑置的時間超過閾值,則會被終止并移出緩存。

  3. 系統長時間閑置的時候,不會消耗什么資源。

newSingleThreadExecutor()
創建唯一的工作線程來執行任務,如果線程異常結束,會有另一個線程取代它。可保證順序執行任務。

newSingleThreadScheduledExecutor()與newScheduledThreadPool(int corePoolSize)
定時或周期性工作調度,兩者的區別在于前者是單一工作線程,后者是多線程

newWorkStealingPool()
內部構建ForkJoinPool,利用working-stealing算法,并行地處理任務,不保證處理順序。

Fork/Join框架:把大任務分割稱若干個小任務并行執行,最終匯總每個小任務后得到大任務結果的框架。

為什么要使用線程池

線程是稀缺資源,如果無限制地創建線程,會消耗系統資源,而線程池可以代替開發者管理線程,一個線程在結束運行后,不會銷毀線程,而是將線程歸還線程池,由線程池再進行管理,這樣就可以對線程進行復用。

所以線程池不但可以降低資源的消耗,還可以提高線程的可管理性。

使用線程池啟動線程

public class Main{
    public static void main(String[] args) {
        ExecutorService newFixThreadPool = Executors.newFixedThreadPool(10);
        newFixThreadPool.execute(new Runnable() {

            @Override
            public void run() {
                // TODO Auto-generated method stub
                System.out.println("通過線程池啟動線程成功");
            }
        });
        newFixThreadPool.shutdown();
    }
}

 

新任務execute執行后的判斷

要知道這個點首先要先說說ThreadPoolExecutor的構造函數,其中有幾個參數:

  1. corePoolSize:核心線程數量。

  2. maximumPoolSize:線程不夠用時能創建的最大線程數。

  3. workQueue:等待隊列。

那么新任務提交后會執行下列判斷:

  1. 如果運行的線程少于corePoolSize,則創建新線程來處理任務,即時線程池中的其它線程是空閑的。

  2. 如果線程池中的數量大于等于corePoolSize且小于maximumPoolSize,則只有當workQueue滿時,才創建新的線程去處理任務。

  3. 如果設置的corePoolSize和maximumPoolSize相同,則創建的線程池大小是固定的,如果此時有新任務提交,若workQueue未滿,則放入workQueue,等待被處理。

  4. 如果運行的線程數大于等于maximumPoolSize,maximumPoolSize,這時如果workQueue已經滿了,則通過handler所指定的策略來處理任務。

handler 線程池飽和策略

  • AbortPolicy:直接拋出異常,默認。

  • CallerRunsPolicy:用調用者所在的線程來執行任務。

  • DiscardOldestPolicy:丟棄隊列中靠最前的任務,并執行當前任務。

  • DiscardPolicy:直接丟棄任務

  • 自定義。

線程池的大小如何選定

這個問題并不是什么秘密,在網上各大技術網站均有文章說明,我就拿一個最受認可的寫上吧

  • CPU密集型:線程數 = 核心數或者核心數+1

  • IO密集型:線程數 = CPU核數*(1+平均等待時間/平均工作時間)

當然這個也不能完全依賴這個公式,更多的是要依賴平時的經驗來操作,這個公式也只是僅供參考而已。


結語

本文提供了一些Java多線程和并發方面最最基礎的知識,適合初學者了解Java多線程的一些基本知識,如果想了解更多的關于并發方面的內容可以看:

https://juejin.im/post/5d8da403f265da5b5d203bf4

 

作者:Object,首發:Java知音

推薦閱讀(點擊即可跳轉閱讀)

1.SpringBoot內容聚合

2.面試題內容聚合

3.設計模式內容聚合

4.Mybatis內容聚合

5.多線程內容聚合

 

posted on 2019-10-14 11:03 Java知音* 閱讀(...) 評論(...) 編輯 收藏

導航

ag二分彩