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,代碼如下:
publicboolean isInterrupted() {//傳遞false,說明不清除中斷標志
return isInterrupted(false);
}
3.boolean interrupted():檢測當前線程是否被中斷,如果是返回 true,否者返回 false,與 isInterrupted 不同的是該方法如果發現當前線程被中斷后會清除中斷標志,并且該函數是 static 方法,可以通過 Thread 類直接調用。另外從下面代碼可以知道 interrupted() 內部是獲取當前調用線程的中斷標志而不是調用 interrupted() 方法的實例對象的中斷標志。
public staticboolean interrupted() {//清除中斷標志
return currentThread().isInterrupted(true);
}
下面看一個線程使用 Interrupted 優雅退出的經典使用例子,代碼如下:
public voidrun(){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 classInterruptTest {public static voidmain(String[] args) throws InterruptedException {
Thread thread= new Thread(newRunnable() {
@Overridepublic voidrun() {//如果當前線程被中斷則退出循環
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");
}
}
運行結果如下:
如上代碼子線程 thread 通過檢查當前線程中斷標志來控制是否退出循環,主線程在休眠 1s 后調用 thread 的 interrupt() 方法設置了中斷標志,所以線程 thread 退出了循環。
總結:中斷一個線程僅僅是設置了該線程的中斷標志,也就是設置了線程里面的一個變量的值,本身是不能終止當前線程運行的,一般程序里面是檢查這個標志的狀態來判斷是否需要終止當前線程。
二.理解線程上下文切換
在多線程編程中,線程個數一般都大于 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 classDeadLockTest1 {//創建資源
private static Object resourceA = newObject();private static Object resourceB = newObject();public static voidmain(String[] args) {//創建線程A
Thread threadA = new Thread(newRunnable() {public voidrun() {
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(newRunnable() {public voidrun() {
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(newRunnable() {public voidrun() {
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 voidmain(String[] args) {
Thread daemonThread= new Thread(newRunnable() {public voidrun() {
}
});//設置為守護線程
daemonThread.setDaemon(true);
daemonThread.start();
}
可知只需要設置線程的 daemon 參數為 true 即可。
下面通過例子來加深用戶線程與守護線程的區別的理解,首先看下面代碼:
/**
* Created by cong on 2018/7/17.*/
public classUserThreadTest {public static voidmain(String[] args) {
Thread thread= new Thread(newRunnable() {public voidrun() {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 classDaemonThreadTest {public static voidmain(String[] args) {
Thread thread= new Thread(newRunnable() {public voidrun() {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 這個函數:
intJNICALL
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);returnret;
}
}while (JNI_FALSE)
上面宏的作用實際是創建了一個名字叫做 DestroyJavaVM 的線程來等待所有用戶線程結束。
在 Tomcat 的 NIO 實現 NioEndpoint 中會開啟一組接受線程用來接受用戶的鏈接請求和一組處理線程負責具體處理用戶請求,那么這些線程是用戶線程還是守護線程呢?下面我們看下 NioEndpoint 的 startInternal 方法,源碼如下:
public voidstartInternal() throws Exception {if (!running) {
running= true;
paused= false;
...//創建處理線程
pollers = newPoller[getPollerThreadCount()];for (int i=0; i
pollers[i]= newPoller();
Thread pollerThread= new Thread(pollers[i], getName() + "-ClientPoller-"+i);
pollerThread.setPriority(threadPriority);
pollerThread.setDaemon(true);//聲明為守護線程
pollerThread.start();
}//啟動接受線程
startAcceptorThreads();
}protected final voidstartAcceptorThreads() {int count =getAcceptorThreadCount();
acceptors= newAcceptor[count];for (int i = 0; i < count; i++) {
acceptors[i]=createAcceptor();
String threadName= getName() + "-Acceptor-" +i;
acceptors[i].setThreadName(threadName);
Thread t= newThread(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 進程結束那么就設置子線程為用戶線程。
總結
以上是生活随笔為你收集整理的Java并发基础总结_Java并发编程笔记之基础总结(二)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: rlm sql mysql.so_UBU
- 下一篇: java字符流解析_Java IO(四)