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

一.線程中斷

Java 中線程中斷是一種線程間協做模式,經過設置線程的中斷標誌並不能直接終止該線程的執行,而是須要被中斷的線程根據中斷狀態自行處理。java

  1.void interrupt() 方法:中斷線程,例如當線程 A 運行時,線程 B 能夠調用線程 A 的 interrupt() 方法來設置線程 A 的中斷標誌爲 true 並當即返回。設置標誌僅僅是設置標誌,線程 A 並無實際被中斷,會繼續往下執行的。若是線程 A 由於調用了 wait 系列函數或者 join 方法或者 sleep 函數而被阻塞掛起,這時候線程 B 調用了線程 A 的 interrupt() 方法,線程 A 會在調用這些方法的地方拋出 InterruptedException 異常而返回。編程

 

  2.boolean isInterrupted():檢測當前線程是否被中斷,若是是返回 true,否者返回 false,代碼以下:多線程

public boolean isInterrupted() {
   //傳遞false,說明不清除中斷標誌
   return isInterrupted(false);
}

  

  3.boolean interrupted():檢測當前線程是否被中斷,若是是返回 true,否者返回 false,與 isInterrupted 不一樣的是該方法若是發現當前線程被中斷後會清除中斷標誌,而且該函數是 static 方法,能夠經過 Thread 類直接調用。另外從下面代碼能夠知道 interrupted() 內部是獲取當前調用線程的中斷標誌而不是調用 interrupted() 方法的實例對象的中斷標誌。併發

public static boolean interrupted() {
    //清除中斷標誌
    return currentThread().isInterrupted(true);
}

下面看一個線程使用 Interrupted 優雅退出的經典使用例子,代碼以下:ide

public void run(){    
    try{    
         ....    
         //線程退出條件
         while(!Thread.currentThread().isInterrupted()&& more work to do){    
                // do more work;    
         }    
    }catch(InterruptedException e){    
                // thread was interrupted during sleep or wait    
    }    
    finally{    
               // cleanup, if required    
    }    
}

下面看一個根據中斷標誌判斷線程是否終止的例子:函數

/**
 * Created by cong on 2018/7/17.
 */
public class InterruptTest {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                //若是當前線程被中斷則退出循環
                while (!Thread.currentThread().isInterrupted())
                    System.out.println(Thread.currentThread() + " hello");
            }
        });
        //啓動子線程
        thread.start();

        //主線程休眠1s,以便中斷前讓子線程輸出點東西
        Thread.sleep(1);

        //中斷子線程
        System.out.println("main thread interrupt thread");
        thread.interrupt();

        //等待子線程執行完畢
        thread.join();
        System.out.println("main is over");

    }
}

運行結果以下:ui

如上代碼子線程 thread 經過檢查當前線程中斷標誌來控制是否退出循環,主線程在休眠 1s 後調用 thread 的 interrupt() 方法設置了中斷標誌,因此線程 thread 退出了循環。spa

總結:中斷一個線程僅僅是設置了該線程的中斷標誌,也就是設置了線程裏面的一個變量的值,自己是不能終止當前線程運行的,通常程序裏面是檢查這個標誌的狀態來判斷是否須要終止當前線程。操作系統

 

二.理解線程上下文切換

在多線程編程中,線程個數通常都大於 CPU 個數,而每一個 CPU 同一時刻只能被一個線程使用,爲了讓用戶感受多個線程是在同時執行,CPU 資源的分配採用了時間片輪轉的策略,也就是給每一個線程分配一個時間片,在時間片內佔用 CPU 執行任務。當前線程的時間片使用完畢後當前就會處於就緒狀態並讓出 CPU 讓其它線程佔用,這就是上下文切換,從當前線程的上下文切換到了其它線程。線程

那麼就有一個問題讓出 CPU 的線程等下次輪到本身佔有 CPU 時候如何知道以前運行到哪裏了?

因此在切換線程上下文時候須要保存當前線程的執行現場,當再次執行時候根據保存的執行現場信息恢復執行現場

線程上下文切換時機:

  1.當前線程的 CPU 時間片使用完畢處於就緒狀態時候;

  2.當前線程被其它線程中斷時候

總結:因爲線程切換是有開銷的,因此並非開的線程越多越好,好比若是機器是4核心的,你開啓了100個線程,那麼同時執行的只有4個線程,這100個線程會來回切換線程上下文來共享這四個 CPU。

 

三.線程死鎖

什麼是線程死鎖呢?

死鎖是指兩個或兩個以上的線程在執行過程當中,因爭奪資源而形成的互相等待的現象,在無外力做用的狀況下,這些線程會一直相互等待而沒法繼續運行下去。

如上圖,線程 A 已經持有了資源1的同時還想要資源2,線程 B 在持有資源2的時候還想要資源1,因此線程1和線程2就相互等待對方已經持有的資源,就進入了死鎖狀態。

那麼產生死鎖的緣由都有哪些,學過操做系統的應該都知道死鎖的產生必須具有如下四個必要條件。

  1.互斥條件:指線程對已經獲取到的資源進行排它性使用,即該資源同時只由一個線程佔用。若是此時還有其它進行請求獲取該資源,則請求者只能等待,直至佔有資源的線程用畢釋放。

  2.請求並持有條件:指一個線程已經持有了至少一個資源,但又提出了新的資源請求,而新資源已被其其它線程佔有,因此當前線程會被阻塞,但阻塞的同時並不釋放本身已經獲取的資源。

  3.不可剝奪條件:指線程獲取到的資源在本身使用完以前不能被其它線程搶佔,只有在本身使用完畢後由本身釋放。

  4.環路等待條件:指在發生死鎖時,必然存在一個線程——資源的環形鏈,即線程集合{T0,T1,T2,···,Tn}中的 T0 正在等待一個 T1 佔用的資源;T1 正在等待 T2 佔用的資源,……Tn正在等待已被 T0 佔用的資源。

 

下面經過一個例子來講明線程死鎖,代碼以下:

/**
 * Created by cong on 2018/7/17.
 */
public class DeadLockTest1 {
    // 建立資源
    private static Object resourceA = new Object();
    private static Object resourceB = new Object();

    public static void main(String[] args) {
        // 建立線程A
        Thread threadA = new Thread(new Runnable() {
            public void run() {
                synchronized (resourceA) {
                    System.out.println(Thread.currentThread() + " get ResourceA");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread() + "waiting get ResourceB");
                    synchronized (resourceB) {
                        System.out.println(Thread.currentThread() + "get ResourceB");
                    }
                }
            }
        });
        // 建立線程B
        Thread threadB = new Thread(new Runnable() {
            public void run() {
                synchronized (resourceB) {
                    System.out.println(Thread.currentThread() + " get ResourceB");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread() + "waiting get ResourceA");
                    synchronized (resourceA) {
                        System.out.println(Thread.currentThread() + "get ResourceA");
                    }
                };
            }
        });
        // 啓動線程
        threadA.start();
        threadB.start();
    }
}

運行結果以下:

下面分析下代碼和結果,其中 Thread-0 是線程 A,Thread-1 是線程 B,代碼首先建立了兩個資源,並建立了兩個線程。

從輸出結果能夠知道線程調度器先調度了線程 A,也就是把 CPU 資源讓給了線程 A,線程 A 調用了 getResourceA() 方法,方法裏面使用 synchronized(resourceA) 方法獲取到了 resourceA 的監視器鎖,而後調用 sleep 函數休眠 1s,休眠 1s 是爲了保證線程 A 在執行 getResourceB 方法前讓線程 B 搶佔到 CPU 執行 getResourceB 方法。

線程 A 調用了 sleep 期間,線程 B 會執行 getResourceB 方法裏面的 synchronized(resourceB),表明線程 B 獲取到了 objectB 對象的監視器鎖資源,而後調用 sleep 函數休眠 1S。

好了,到了這裏線程 A 獲取到了 objectA 的資源,線程 B 獲取到了 objectB 的資源。線程 A 休眠結束後會調用 getResouceB 方法企圖獲取到 ojbectB 的資源,而 ObjectB 資源被線程 B 所持有,因此線程 A 會被阻塞而等待。而同時線程 B 休眠結束後會調用 getResourceA 方法企圖獲取到 objectA 上的資源,而資源 objectA 已經被線程 A 持有,因此線程 A 和 B 就陷入了相互等待的狀態也就產生了死鎖。

 

下面從產生死鎖的四個條件來談談本案例如何知足了四個條件。

首先資源 resourceA 和 resourceB 都是互斥資源,當線程 A 調用 synchronized(resourceA) 獲取到 resourceA 上的監視器鎖後釋放前,線程 B 在調用 synchronized(resourceA) 嘗試獲取該資源會被阻塞,只有線程 A 主動釋放該鎖,線程 B 才能得到,這知足了資源互斥條件。

線程 A 首先經過 synchronized(resourceA) 獲取到 resourceA 上的監視器鎖資源,而後經過 synchronized(resourceB) 等待獲取到 resourceB 上的監視器鎖資源,這就構造了持有並等待。

線程 A 在獲取 resourceA 上的監視器鎖資源後,不會被線程 B 掠奪走,只有線程 A 本身主動釋放 resourceA 的資源時候,纔會放棄對該資源的持有權,這構造了資源的不可剝奪條件。

線程 A 持有 objectA 資源並等待獲取 objectB 資源,而線程 B 持有 objectB 資源並等待 objectA 資源,這構成了循環等待條件。

因此線程 A 和 B 就造成了死鎖狀態。

那麼如何避免線程死鎖呢?

要想避免死鎖,須要破壞構造死鎖必要條件的至少一個便可,可是學過操做系統童鞋應該都知道目前只有持有並等待和循環等待是能夠被破壞的。

形成死鎖的緣由其實和申請資源的順序有很大關係,使用資源申請的有序性原則就能夠避免死鎖,那麼什麼是資源的有序性呢,先看一下對上面代碼的修改:

     // 建立線程B
        Thread threadB = new Thread(new Runnable() {
            public void run() {
                synchronized (resourceA) {
                    System.out.println(Thread.currentThread() + " get ResourceB");

                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    System.out.println(Thread.currentThread() + "waiting get ResourceA");
                    synchronized (resourceB) {
                        System.out.println(Thread.currentThread() + "get ResourceA");
                    }
                };
            }
        });

運行結果以下:

如上代碼可知修改了線程 B 中獲取資源的順序和線程 A 中獲取資源順序一致,其實資源分配有序性就是指假如線程 A 和 B 都須要資源1,2,3……n 時候,對資源進行排序,線程 A 和 B 只有在獲取到資源 n-1 時候才能去獲取資源 n。

總結:編寫併發程序,多個線程進行共享多個資源時候要注意採用資源有序分配法避免死鎖的產生。

 

四守護線程與用戶線程

Java 中線程分爲兩類,分別爲 Daemon 線程(守護線程)和 User 線程(用戶線程),在 JVM 啓動時候會調用 main 函數,main 函數所在的線程是一個用戶線程,這個是咱們能夠看到的線程,其實 JVM 內部同時還啓動了好多守護線程,好比垃圾回收線程(嚴格說屬於 JVM 線程)。

那麼守護線程和用戶線程有什麼區別呢?

區別之一是當最後一個非守護線程結束時候,JVM 會正常退出,而無論當前是否有守護線程;也就是說守護線程是否結束並不影響 JVM 的退出。言外之意是隻要有一個用戶線程還沒結束正常狀況下 JVM 就不會退出。

那麼 Java 中如何建立一個守護線程呢?代碼以下:

public static void main(String[] args) {

        Thread daemonThread = new Thread(new  Runnable() {
            public void run() {

            }
        });

        //設置爲守護線程
        daemonThread.setDaemon(true);
        daemonThread.start();

} 

可知只須要設置線程的 daemon 參數爲 true 便可。

下面經過例子來加深用戶線程與守護線程的區別的理解,首先看下面代碼:

/**
 * Created by cong on 2018/7/17.
 */
public class UserThreadTest {
    public static void main(String[] args) {

        Thread thread = new Thread(new  Runnable() {
            public void run() {
                for(;;){}
            }
        });

        //啓動子線
        thread.start();

        System.out.print("main thread is over");
    }
}

運行結果以下:

如上代碼在 main 線程中建立了一個 thread 線程,thread 線程裏面是無限循環,運行代碼從結果看 main 線程已經運行結束了,那麼 JVM 進程已經退出了?從 IDE 的輸出結側上的紅色方塊說明 JVM 進程並無退出,另外 Mac 上執行 ps -eaf | grep java 會輸出結果,也能夠證實這個結論。

這個結果說明了當父線程結束後,子線程仍是能夠繼續存在的,也就是子線程的生命週期並不受父線程的影響。也說明了當用戶線程還存在的狀況下 JVM 進程並不會終止。

那麼咱們把上面的 thread 線程設置爲守護線程後在運行看看會有什麼效果,代碼以下:

/**
 * Created by cong on 2018/7/17.
 */
public class DaemonThreadTest {
    public static void main(String[] args) {
        Thread thread = new Thread(new  Runnable() {
            public void run() {
                for(;;){}
            }
        });
        //設置爲守護線程
        thread.setDaemon(true);
        //啓動子線
        thread.start();
        System.out.print("main thread is over");
    }
}

運行結果以下:

如上在啓動線程前設置線程爲守護線程,從輸出結果可知 JVM 進程已經終止了,執行 ps -eaf |grep java 也看不到 JVM 進程了。這個例子裏面 main 函數是惟一的用戶線程,thread 線程是守護線程,當 main 線程運行結束後,JVM 發現當前已經沒有用戶線程了,就會終止 JVM 進程。

Java 中在 main 線程運行結束後,JVM 會自動啓動一個叫作 DestroyJavaVM 線程,該線程會等待全部用戶線程結束後終止 JVM 進程。

下面經過簡單的 JVM 代碼來證實這個結論,翻開 JVM 的代碼,最終會調用到 JavaMain 這個函數:

int JNICALL
JavaMain(void * _args)
{   
    ...
    //執行Java中的main函數 
    (*env)->CallStaticVoidMethod(env, mainClass, mainID, mainArgs);

    //main函數返回值
    ret = (*env)->ExceptionOccurred(env) == NULL ? 0 : 1;

    //等待全部非守護線程結束,而後銷燬JVM進程
    LEAVE();
}

LEAVE 是 C 語言裏面的一個宏定義,定義以下:

#define LEAVE() 
    do { 
        if ((*vm)->DetachCurrentThread(vm) != JNI_OK) { 
            JLI_ReportErrorMessage(JVM_ERROR2); 
            ret = 1; 
        } 
        if (JNI_TRUE) { 
            (*vm)->DestroyJavaVM(vm); 
            return ret; 
        } 
    } while (JNI_FALSE)

上面宏的做用實際是建立了一個名字叫作 DestroyJavaVM 的線程來等待全部用戶線程結束。

在 Tomcat 的 NIO 實現 NioEndpoint 中會開啓一組接受線程用來接受用戶的連接請求和一組處理線程負責具體處理用戶請求,那麼這些線程是用戶線程仍是守護線程呢?下面咱們看下 NioEndpoint 的 startInternal 方法,源碼以下:

  public void startInternal() throws Exception {

        if (!running) {
            running = true;
            paused = false;

            ...

            //建立處理線程
            pollers = new Poller[getPollerThreadCount()];
            for (int i=0; i<pollers.length; i++) {
                pollers[i] = new Poller();
                Thread pollerThread = new Thread(pollers[i], getName() + "-ClientPoller-"+i);
                pollerThread.setPriority(threadPriority);
                pollerThread.setDaemon(true);//聲明爲守護線程
                pollerThread.start();
            }
            //啓動接受線程
            startAcceptorThreads();
    }
protected final void startAcceptorThreads() { int count = getAcceptorThreadCount(); acceptors = new Acceptor[count]; for (int i = 0; i < count; i++) { acceptors[i] = createAcceptor(); String threadName = getName() + "-Acceptor-" + i; acceptors[i].setThreadName(threadName); Thread t = new Thread(acceptors[i], threadName); t.setPriority(getAcceptorThreadPriority()); t.setDaemon(getDaemon());//設置是否爲守護線程,默認爲守護線程 t.start(); } } private boolean daemon = true; public void setDaemon(boolean b) { daemon = b; } public boolean getDaemon() { return daemon; }

如上代碼也就是說默認狀況下接受線程和處理線程都是守護線程,這意味着當 Tomact 收到 shutdown 命令後 Tomact 進程會立刻消亡,而不會等處理線程處理完當前的請求。

總結:若是你想在主線程結束後 JVM 進程立刻結束,那麼建立線程的時候能夠設置線程爲守護線程,不然若是但願主線程結束後子線程繼續工做,等子線程結束後在讓 JVM 進程結束那麼就設置子線程爲用戶線程。

相關文章
相關標籤/搜索