1. 程式人生 > >多執行緒基礎體系知識清單

多執行緒基礎體系知識清單

前言

本文會介紹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.多執行緒內容聚合

&n