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


一.線程概念

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

Java 中當我們啟動 main 函數時候其實就啟動了一個 JVM 的進程,而 main 函數所在線程就是這個進程中的一個線程,也叫做主線程。

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

 

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

前面說了線程是占用 CPU 執行的基本單位,而 CPU 一般是使用時間片輪轉方式讓線程輪詢占用的,所以當前線程 CPU 時間片用完后,要讓出 CPU,等下次輪到自己時候在執行。

那么如何知道之前程序執行到哪里了?

其實程序計數器就是為了記錄該線程讓出 CPU 時候的執行地址,待再次分配到時間片時候就可以從自己私有的計數器指定地址繼續執行了。

 

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

堆是一個進程中最大的一塊內存,堆是被進程中的所有線程共享的,是進程創建時候分配的,堆里面主要存放使用 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 執行權,但是該線程所擁有的監視器資源,比如鎖還是持有不讓出的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM