Java併發編程筆記之基礎總結(一)

一.線程概念

說到線程就必需要提一下進程,由於線程是進程中的一個實體,線程自己是不會獨立存在的。進程是代碼在數據集合上的一次運行活動,是系統進行資源分配和調度的基本單位,線程則是進程的一個執行路徑,一個進程至少有一個線程,進程中的多個線程是共享進程的資源的。操做系統在分配資源時候是把資源分配給進程的,可是 CPU 資源就比較特殊,它是分派到線程的,由於真正要佔用 CPU 運行的是線程,因此也說線程是 CPU 分配的基本單位。java

Java 中當咱們啓動 main 函數時候其實就啓動了一個 JVM 的進程,而 main 函數所在線程就是這個進程中的一個線程,也叫作主線程。併發

如上圖一個進程中有多個線程,多個線程共享進程的堆和方法區資源,可是每一個線程有本身的程序計數器,棧區域。、異步

 

其中程序計數器是一塊內存區域,用來記錄線程當前要執行的指令地址,那麼程序計數器爲什麼要設計爲線程私有的呢?ide

前面說了線程是佔用 CPU 執行的基本單位,而 CPU 通常是使用時間片輪轉方式讓線程輪詢佔用的,因此當前線程 CPU 時間片用完後,要讓出 CPU,等下次輪到本身時候在執行。函數

那麼如何知道以前程序執行到哪裏了?測試

其實程序計數器就是爲了記錄該線程讓出 CPU 時候的執行地址,待再次分配到時間片時候就能夠從本身私有的計數器指定地址繼續執行了。this

 

另外每一個線程有本身的棧資源,用於存儲該線程的局部變量,這些局部變量是該線程私有的,其它線程是訪問不了的,另外棧還用來存放線程的調用棧幀。spa

堆是一個進程中最大的一塊內存,堆是被進程中的全部線程共享的,是進程建立時候分配的,堆裏面主要存放使用 new 操做建立的對象實例。操作系統

方法區則是用來存放進程中的代碼片斷的,是線程共享的。線程

 

二.線程建立方式與運行

Java 中有三種線程建立方法,分別爲實現 Runnable 接口的run方法、繼承 Thread 類並重寫 run 方法、使用 FutureTask 方式。

  1.繼承 Thread 方法的實現,以下所示:

/**
 * Created by cong on 2018/7/17.
 */
public class ThreadTest {
    //繼承Thread類並重寫run方法
    public static class MyThread extends Thread {

        @Override
        public void run() {

            System.out.println("-----子線程-----");

        }
    }

    public static void main(String[] args) {

        // 建立線程
        MyThread thread = new MyThread();

        // 啓動線程
        thread.start();
    }
}

運行結果以下:

如上代碼 MyThread 類繼承了 Thread 類,並重寫了 run 方法,而後調用了線程的 start 方法啓動了線程,當建立完 thread 對象後該線程並無被啓動執行.當調用了 start 方法後纔是真正啓動了線程。其實當調用了 start 方法後線程並無立刻執行而是處於就緒狀態,這個就緒狀態是指該線程已經獲取了除 CPU 資源外的其它資源,等獲取 CPU 資源後纔會真正處於運行狀態。

當 run 方法執行完畢,該線程就處於終止狀態了。使用繼承方式好處是 run 方法內獲取當前線程直接使用 this 就能夠,無須使用 Thread.currentThread() 方法,很差的地方是 Java 不支持多繼承,若是繼承了 Thread 類那麼就不能再繼承其它類,另外任務與代碼沒有分離,當多個線程執行同樣的任務時候須要多份任務代碼,而 Runable 則沒有這個限制。

  2.實現 Runnable 接口的 run 方法方式,例子以下所示:

/**
 * Created by cong on 2018/7/17.
 */
public class RunableTest implements Runnable {
    @Override
    public void run() {
        System.out.println("----子線程----");
    }

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

        RunableTest runableTest = new RunableTest();
        new Thread(runableTest).start();
        new Thread(runableTest).start();
    }
}

運行結果以下:

如上面代碼,兩個線程公用一個 task 代碼邏輯,須要的話 RunableTask 能夠添加參數進行任務區分,另外 RunableTask 能夠繼承其餘類,可是上面兩種方法都有一個缺點就是任務沒有返回值,

 

  3.使用 FutureTask方式,例子以下所示:

/**
 * Created by cong on 2018/7/17.
 */
public class FutureTaskTest implements Callable<String> {
    @Override
    public String call() throws Exception {

        return "hello";
    }

    public static void main(String[] args) throws InterruptedException {
        // 建立異步任務
        FutureTask<String> futureTask = new FutureTask<>(new FutureTaskTest());
        //啓動線程
        new Thread(futureTask).start();
        try {
            //等待任務執行完畢,並返回結果
            String result = futureTask.get();
            System.out.println(result);
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
    }
}

運行結果以下:

 

總結:每種方式都有本身的優缺點,應該根據實際場景進行選擇

 

三.線程通知與等待

Java 中 Object 類是全部類的父類,鑑於繼承機制,Java 把全部類都須要的方法放到了 Object 類裏面,其中就包含本節要講的通知等待系列函數,這些通知等待函數是組成併發包中線程同步組件的基礎。

下面講解下 Object 中關於線程同步的通知等待函數。以下所示:

  1.void wait() 方法:首先談下什麼是共享資源,所謂共享資源是說該資源被多個線程共享,多個線程均可以去訪問或者修改的資源。當一個線程調用一個共享對象的 wait() 方法時候,調用線程會被阻塞掛起,直到下面幾個事情之一發生才返回:

  1. 其它線程調用了該共享對象的 notify() 或者 notifyAll() 方法;
  2. 其它線程調用了該線程的 interrupt() 方法設置了該線程的中斷標誌,該線程會拋出 InterruptedException 異常返回

另外須要注意的是若是調用 wait() 方法的線程沒有事先獲取到該對象的監視器鎖,則調用 wait() 方法時候調用線程會拋出 IllegalMonitorStateException 異常。

那麼一個線程如何獲取到一個共享變量的監視器呢?

  1.執行使用 synchronized 同步代碼塊時候,使用該共享變量做爲參數,例子以下:

synchronized(共享變量){
   //doSomething
}

  2.調用該共享變量的方法,而且該方法使用了 synchronized 修飾,代碼以下:

synchronized void add(int a,int b){
   //doSomething
}

另外須要注意的是一個線程能夠從掛起狀態變爲能夠運行狀態(也就是被喚醒)即便該線程沒有被其它線程調用 notify(),notifyAll() 進行通知,或者被中斷,或者等待超時,這就是所謂的虛假喚醒

雖然虛假喚醒在應用實踐中不多發生,可是仍是須要防範於未然的,作法就是不停的去測試該線程被喚醒的條件是否知足,不知足則繼續等待,也就是說在一個循環中去調用 wait() 方法進行防範,退出循環的條件是條件知足了喚醒該線程。代碼以下:

 synchronized (obj) {
     while (條件不知足){
         obj.wait();  
     }
 }

如上代碼是經典的調用共享變量 wait() 方法的實例,首先經過同步塊獲取 obj 上面的監視器鎖,而後經過 while 循環內調用 obj 的 wait() 方法。

下面從生產者消費者例子來加深理解,例子以下:

  生產者:

//生產線程
synchronized (queue) { 

    //消費隊列滿,則等待隊列空閒
    while (queue.size() == MAX_SIZE) { 
        try { 
            //掛起當前線程,並釋放經過同步塊獲取的queue上面的鎖,讓消費線程能夠獲取該鎖,而後獲取隊列裏面元素
            queue.wait(); 
        } catch (Exception ex) { 
            ex.printStackTrace(); 
        } 
    }

    //空閒則生成元素,並通知消費線程
    queue.add(ele); 
    queue.notifyAll(); 

    } 
} 

 

  消費者:

//消費線程
synchronized (queue) { 

    //消費隊列爲空
    while (queue.size() == 0) { 
        try {
            //掛起當前線程,並釋放經過同步塊獲取的queue上面的鎖,讓生產線程能夠獲取該鎖,生產元素放入隊列
            queue.wait(); 
        } catch (Exception ex) { 
            ex.printStackTrace(); 
        } 
    }

    //消費元素,並通知喚醒生產線程
    queue.take(); 
    queue.notifyAll();  
} 

如上面代碼所示是一個生產者的例子,其中 queue 爲共享變量,生產者線程在調用 queue 的 wait 方法前,經過使用 synchronized 關鍵字拿到了該共享變量 queue 的監視器,因此調用 wait() 方法纔不會拋出 IllegalMonitorStateException 異常,若是當前隊列沒有空閒容量則會調用 queued 的 wait() 掛起當前線程,這裏使用循環就是爲了不上面說的虛假喚醒問題,這裏假如當前線程虛假喚醒了,可是隊列仍是沒有空餘容量的話,當前線程仍是會調用 wait() 把本身掛起。

另外當一個線程調用了共享變量的 wait() 方法後該線程會被掛起,同時該線程會暫時釋放對該共享變量監視器的持有,直到另一個線程調用了共享變量的 notify() 或者 notifyAll() 方法纔有可能會從新獲取到該共享變量的監視器的持有權(這裏說有可能,是由於考慮到多個線程第一次都調用了 wait() 方法,因此多個線程會競爭持有該共享變量的監視器)。、

 

接下來說解下調用共享變量 wait() 方法後當前線程會釋放持有的共享變量的鎖的理解。

如上代碼假如生產線程 A 首先經過 synchronized 獲取到了 queue 上的鎖,那麼其它生產線程和全部消費線程都會被阻塞,線程 A 獲取鎖後發現當前隊列已滿會調用 queue.wait() 方法阻塞本身,而後會釋放獲取的 queue 上面的鎖,這裏考慮下爲什麼要釋放該鎖?若是不釋放,因爲其它生產線程和全部消費線程已經被阻塞掛起,而線程 A 也被掛起,這就處於了死鎖狀態。這裏線程 A 掛起本身後釋放共享變量上面的鎖就是爲了打破死鎖必要條件之一的持有並等待原則。關於死鎖下面章節會有講到,線程 A 釋放鎖後其它生產線程和全部消費線程中會有一個線程獲取 queue 上的鎖進而進入同步塊,這就打破了死鎖。

最後再舉一個例子說明當一個線程調用共享對象的 wait() 方法被阻塞掛起後,若是其它線程中斷了該線程,則該線程會拋出 InterruptedException 異常後返回,代碼以下:

/**
 * Created by cong on 2018/7/17.
 */
public class WaitNotifyInteruptTest {
    static Object obj = new Object();

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

        //建立線程
        Thread threadA = new Thread(new Runnable() {
            public void run() {
                try {
                    System.out.println("---開始---");
                    //阻塞當前線程
                    obj.wait();
                    System.out.println("---結束---");

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        threadA.start();

        Thread.sleep(1000);

        System.out.println("---開始打斷線程A---");
        threadA.interrupt();
        System.out.println("---線程A已經被打斷---");
    }
}

運行結果以下:

如上代碼 threadA 調用了共享對 obj 的 wait() 方法後阻塞掛起了本身,而後主線程在休眠1s後中斷了 threadA 線程,可知中斷後 threadA 在 obj.wait() 處拋出了 java.lang.IllegalMonitorStateException 異常後返回後終止。

 

  2.void wait(long timeout) 方法:該方法相比 wait() 方法多一個超時參數,不一樣在於若是一個線程調用了共享對象的該方法掛起後,若是沒有在指定的 timeout ms 時間內被其它線程調用該共享變量的 notify() 或者 notifyAll() 方法喚醒,那麼該函數仍是會由於超時而返回。須要注意的是若是在調用該函數時候 timeout 傳遞了負數會拋出 IllegalArgumentException 異常。

 

  3.void wait(long timeout, int nanos) 方法:內部是調用 wait(long timeout),以下代碼:只是當 nanos>0 時候讓參數一遞增1。源碼以下:

public final void wait(long timeout, int nanos) throws InterruptedException {
        if (timeout < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }

        if (nanos < 0 || nanos > 999999) {
            throw new IllegalArgumentException(
                                "nanosecond timeout value out of range");
        }

        if (nanos > 0) {
            timeout++;
        }

        wait(timeout);
}

 

  4.void notify() 方法:一個線程調用共享對象的 notify() 方法後,會喚醒一個在該共享變量上調用 wait 系列方法後被掛起的線程,一個共享變量上可能會有多個線程在等待,具體喚醒哪個等待的線程是隨機的。另外被喚醒的線程不能立刻從 wait 返回繼續執行,它必須獲取了共享對象的監視器後才能夠返回,也就是喚醒它的線程釋放了共享變量上面的監視器鎖後,被喚醒它的線程也不必定會獲取到共享對象的監視器,這是由於該線程還須要和其它線程一塊競爭該鎖,只有該線程競爭到了該共享變量的監視器後才能夠繼續執行。

相似 wait 系列方法,只有當前線程已經獲取到了該共享變量的監視器鎖後,才能夠調用該共享變量的 notify() 方法,否者會拋出 IllegalMonitorStateException 異常。

 

  5.void notifyAll() 方法:不一樣於 nofity() 方法在共享變量上調用一次就會喚醒在該共享變量上調用 wait 系列方法被掛起的一個線程,notifyAll() 則會喚醒全部在該共享變量上因爲調用 wait 系列方法而被掛起的線程。

最後講一個例子來講明 notify() 和 notifyAll() 的具體含義和一些須要注意的地方,代碼實例以下:

/**
 * Created by cong on 2018/7/17.
 */
public class Test1 {
    private static volatile Object resourceA = new Object();

    public static void main(String[] args) throws InterruptedException {
        // 建立線程
        Thread threadA = new Thread(new Runnable() {
            public void run() {
                // 獲取resourceA共享資源的監視器鎖
                synchronized (resourceA) {
                    System.out.println("threadA get resourceA lock");
                    try {
                        System.out.println("threadA begin wait");
                        resourceA.wait();
                        System.out.println("threadA end wait");

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

        // 建立線程
        Thread threadB = new Thread(new Runnable() {
            public void run() {
                synchronized (resourceA) {
                    System.out.println("threadB get resourceA lock");
                    try {
                        System.out.println("threadB begin wait");
                        resourceA.wait();
                        System.out.println("threadB end wait");
                    } catch (InterruptedException e) {
                        // TODO Auto-generated catch block
                        e.printStackTrace();
                    }
                }
            }

        });

        // 建立線程
        Thread threadC = new Thread(new Runnable() {
            public void run() {
                synchronized (resourceA) {
                    System.out.println("threadC begin notify");
                    resourceA.notifyAll();
                }
            }
        });

        // 啓動線程
        threadA.start();
        threadB.start();

        Thread.sleep(1000);
        threadC.start();

        // 等待線程結束
        threadA.join();
        threadB.join();
        threadC.join();
        System.out.println("main over");
    }
}

運行結果以下:

 

如上代碼開啓了三個線程,其中線程 A 和 B 分別調用了共享資源 resourceA 的 wait() 方法,線程 C 則調用了 nofity() 方法。

這裏啓動線程 C 前首先調用 sleep 方法讓主線程休眠 1s,目的是讓線程 A 和 B 所有執行到調用 wait 方法後在調用線程 C 的 notify 方法。

這個例子企圖但願在線程 A 和線程 B 都因調用共享資源 resourceA 的 wait() 方法而被阻塞後,線程 C 在調用 resourceA 的 notify() 方法,但願能夠喚醒線程 A 和線程 B,可是從執行結果看只有一個線程 A 被喚醒了,線程 B 沒有被喚醒,

從結果看線程調度器此次先調度了線程 A 佔用 CPU 來運行,線程 A 首先獲取 resourceA 上面的鎖,而後調用 resourceA 的 wait() 方法掛起當前線程並釋放獲取到的鎖,而後線程 B 獲取到 resourceA 上面的鎖並調用了 resourceA 的 wait(),此時線程 B 也被阻塞掛起並釋放了 resourceA 上的鎖。

線程 C 休眠結束後在共享資源 resourceA 上調用了 notify() 方法,則會激活 resourceA 的阻塞集合裏面的一個線程,這裏激活了線程 A,因此線程 A 調用的 wait() 方法返回了,線程 A 執行完畢。而線程 B 還處於阻塞狀態。

若是把線程 C 裏面調用的 notify() 改成調用 notifyAll() 而執行結果以下:

可知線程 A 和線程 B 被掛起後,線程 C 調用 notifyAll() 函數會喚醒在 resourceA 等待的全部線程,這裏線程 A 和線程 B 都會被喚醒,只是線程 B 先獲取到 resourceA 上面的鎖而後從 wait() 方法返回,等線程 B 執行完畢後,線程 A 又獲取了 resourceA 上面的鎖,而後從 wait() 方返回,當線程 A 執行完畢,主線程就返回後,而後打印輸出。

總結:在調用具體共享對象的 wait 或者 notify 系列函數前要先獲取共享對象的鎖;另外通知和等待是實現線程同步的原生方法,理解它們的協做功能頗有必要;最後因爲線程虛假喚醒的存在,必定要使用循環檢查的方式。

 

  6.等待線程執行終止的 join 方法:在項目實踐時候常常會遇到一個場景,就是須要等待某幾件事情完成後才能繼續往下執行,好比多個線程去加載資源,當多個線程所有加載完畢後在彙總處理,Thread 類中有個靜態的 join 方法就能夠作這個事情,前面介紹的等待通知方法是屬於 Object 類的,而 join 方法則是直接在 Thread 類裏面提供的,join 是無參,返回值爲 void 的方法。下面看一個簡單的例子來介紹 join 的使用:

/**
 * Created by cong on 2018/7/17.
 */
public class Test3 {
    public static void main(String[] args) throws InterruptedException {
        Thread threadOne = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("child threadOne over!");
            }
        });

        Thread threadTwo = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("child threadTwo over!");
            }
        });
        //啓動子線程
        threadOne.start();
        threadTwo.start();
        System.out.println("wait all child thread over!");
        //等待子線程執行完畢,返回
        threadOne.join();
        threadTwo.join();
        System.out.println("all child thread over!");
    }
}

運行結果以下:

如代碼主線程裏面啓動了兩個子線程,而後在分別調用了它們的 join() 方法,那麼主線程首先會阻塞到 threadOne.join() 方法,等 threadOne 執行完畢後返回,threadOne 執行完畢後 threadOne.join() 就會返回,而後主線程調用 threadTwo.join() 後再次被阻塞,等 threadTwo 執行完畢後主線程也就返回了。這裏只是爲了演示 join 的做用,對應這類需求後面會講的 CountDownLatch 是不錯選擇。

另外線程 A 調用線程 B 的 join 方法後會被阻塞,當其它線程調用了線程 B 的 interrupt() 方法中斷了線程 B 時候,線程 B 會拋出 InterruptedException 異常而返回,下面經過一個例子來加深理解:

/**
 * Created by cong on 2018/7/17.
 */
public class Test4 {
    public static void main(String[] args) throws InterruptedException {
        //線程one
        Thread threadOne = new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("threadOne begin run!");
                for (;;) {
                }
            }
        });
        //獲取主線程
        final Thread mainThread = Thread.currentThread();
        //線程two
        Thread threadTwo = new Thread(new Runnable() {
            @Override
            public void run() {
                //休眠1s
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                //中斷主線程
                mainThread.interrupt();
            }
        });
        // 啓動子線程
        threadOne.start();
        //延遲1s啓動線程
        threadTwo.start();
        try{//等待線程one執行結束
            threadOne.join();

        }catch(InterruptedException e){
            System.out.println("main thread:" + e);
        }
    }
}

運行結果以下:

如上代碼 threadOne 線程裏面執行死循環,主線程調用 threadOne 的 join 方法阻塞本身等待線程 threadOne 執行完畢,待 threadTwo 休眠 1s 後會調用主線程的 interrupt() 方法設置主線程的中斷標誌。

從結果看主線程中 threadOne.join() 處會拋出 InterruptedException 異常而返回。這裏須要注意的是 threadTwo 裏面調用的是主線程的 interrupt(),而不是線程 threadOne 的。

總結:因爲 CountDownLatch 功能比 join 更豐富,因此項目實踐中通常使用 CountDownLatch。

 

  7.讓線程睡眠的 sleep 方法:Thread 類中有一個靜態的 sleep 方法,當一個執行中的線程調用了 Thread 的 sleep 方法後,調用線程會暫時讓出指定時間的執行權,也就是這期間不參與 CPU 的調度,可是該線程所擁有的監視器資源,好比鎖仍是持有不讓出的。當指定的睡眠時間到了該函數會正常返回,線程就處於就緒狀態,而後參與 CPU 的調度,當獲取到了 CPU 資源就能夠繼續運行了。若是在睡眠期間其它線程調用了該線程的 interrupt() 方法中斷了該線程,該線程會在調用 sleep 的地方拋出 InterruptedException 異常返回。

用一個例子來講明線程在睡眠時候擁有的監視器資源不會被釋放是什麼意思,例子以下:

/**
 * Created by cong on 2018/7/17.
 */
public class SleepTest2 {
    // 建立一個獨佔鎖
    private static final Lock lock = new ReentrantLock();
    
    public static void main(String[] args) throws InterruptedException {
        // 建立線程A
        Thread threadA = new Thread(new Runnable() {
            public void run() {
                // 獲取獨佔鎖
                lock.lock();
                try {
                    System.out.println("child threadA is in sleep");

                    Thread.sleep(10000);

                    System.out.println("child threadA is in awaked");

                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 釋放鎖
                    lock.unlock();
                }
            }
        });
        // 建立線程B
        Thread threadB = new Thread(new Runnable() {
            public void run() {
                // 獲取獨佔鎖
                lock.lock();
                try {
                    System.out.println("child threadB is in sleep");
                    Thread.sleep(10000);
                    System.out.println("child threadB is in awaked");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    // 釋放鎖
                    lock.unlock();
                }
            }
        });
        // 啓動線程
        threadA.start();
        threadB.start();
    }
}

運行結果以下:

如上代碼首先建立了一個獨佔鎖,而後建立了兩個線程,每一個線程內部先獲取鎖,而後睡眠,睡眠結束後會釋放鎖。

首先不管你執行多少遍上面的代碼都是先輸出線程 A 的打印或者先輸出線程 B 的打印,不會存在線程 A 和線程 B 交叉打印的狀況。

從執行結果看線程 B 先獲取了鎖,那麼線程 B 會先打印一行,而後調用 sleep 讓本身沉睡 10s,在線程 B 沉睡的這 10s 內那個獨佔鎖 lock 仍是線程 B 本身持有的,線程 A 會一直阻塞直到線程 B 醒過來後執行 unlock 釋放鎖。

下面在來看下當一個線程處於睡眠時候若是另一個線程中斷了它,會不會在調用 sleep 處拋出異常。代碼以下:

/**
 * Created by cong on 2018/7/17.
 */
public class SleepInterruptTest {
    public static void main(String[] args) throws InterruptedException {
        //建立線程
        Thread thread = new Thread(new  Runnable() {
            public void run() {
                try {
                    System.out.println("child thread is in sleep");
                    Thread.sleep(10000);
                    System.out.println("child thread is in awaked");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        //啓動線程
        thread.start();

        //主線程休眠2s
        Thread.sleep(2000);

        //主線程中斷子線程
        thread.interrupt();
    }
}

如上代碼在子線程睡眠期間主線程中斷了它,因此子線程在調用 sleep 處拋出了 InterruptedException 異常。

總結:sleep 方法只是會讓調用線程暫時讓出指定時間的 CPU 執行權,可是該線程所擁有的監視器資源,好比鎖仍是持有不讓出的。

相關文章
相關標籤/搜索