文章目錄
1 Java中關於應用程序和進程相關的概念
在Java中,一個應用程序對應着一個JVM實例(也有地方稱爲JVM進程),一般來說名字默認爲java.exe或者javaw.exe(windows下可以通過任務管理器查看)。
Java採用的是 單線程編程模型
,即在 我們自己的程序中如果沒有主動創建線程的話,只會創建一個線程,通常稱爲主線程
。但是要注意,雖然只有一個線程來執行任務,不代表JVM中只有一個線程,JVM實例在創建的時候,同時會創建很多其他的線程(比如垃圾收集器線程)。
由於Java採用的是單線程編程模型,因此在進行UI編程時要注意將耗時的操作放在子線程中進行,以避免阻塞主線程(在UI編程時,主線程即UI線程,用來處理用戶的交互事件)。
2 線程的特點
線程是進程的組成部分,一個進程可以擁有多個線程,而一個線程必須擁有一個父進程。線程可以擁有自己的堆棧,自己的程序計數器和自己的局部變量,但不能擁有系統資源。
它與父進程的其他線程共享該進程的所有資源。
- 線程可以完成一定任務,可以和其它線程共享父進程的共享變量和部分環境,相互協作來完成任務
- 線程是獨立運行的,其不知道進程中是否還有其他線程存在
- 線程的執行是搶佔式的,也就是說,當前執行的線程隨時可能被掛起,以便運行另一個線程
- 一個線程可以創建或撤銷另一個線程,一個
在這裏插入代碼片
進程中的多個線程可以併發執行
3 線程的創建
Java使用Thread類代表線程
,所有的線程對象都必須是Thread或者其子類的實例
,每個線程的作用是完成一定任務,實際上是就是執行一段程序流(一段順序執行的代碼)
3.1 繼承Thread類創建線程類
- 定義Thread類的子類 並重寫該類的Run方法
該run方法的方法體就代表了線程需要完成的任務
- 創建Thread類的實例,即創建了線程對象
- 調用線程的start方法來啓動線程
class MyThread extends Thread{
private static int num = 0;
public MyThread(){
num++;
}
public void run() {
System.out.println("主動創建的第"+num+"個線程");
}
}
通過start()方法去啓動線程,注意,不是調用run()方法啓動線程,run方法中只是定義需要執行的任務,如果調用run方法,即相當於在主線程中執行run方法,跟普通的方法調用沒有任何區別,此時並不會創建一個新的線程來執行定義的任務。爲了分清start()方法調用和run()方法調用的區別,請看下面一個例子:
public class Test {
public static void main(String[] args) {
System.out.println("主線程ID:"+Thread.currentThread().getId());
MyThread thread1 = new MyThread("thread1");
thread1.start();
MyThread thread2 = new MyThread("thread2");
thread2.run();
}
}
class MyThread extends Thread{
private String name;
public MyThread(String name){
this.name = name;
}
public void run() {
System.out.println("name:"+name+" 子線程ID:"+Thread.currentThread().getId());
}
}
運行結果
主線程ID:1
name:thread2 子線程ID:1
name:thread1 子線程ID:8
從輸出結果可以得出以下結論:
1)thread1和thread2的線程ID不同,thread2和主線程ID相同,說明通過run方法調用並不會創建新的線程,而是在主線程中直接運行run方法,跟普通的方法調用沒有任何區別;
2)雖然thread1的start方法調用在thread2的run方法前面調用,但是先輸出的是thread2的run方法調用的相關信息,說明新線程創建的過程不會阻塞主線程的後續執行
3.2 實現 Runnable接口
Runnable對象僅作爲Thread對象的target,Runnable實現類裏包含的run()方法僅作爲線程執行體。而實際的線程的對象依舊是 Thread實例
,只是線程實例負責執行其 target 的 run()方法
- 定義Runnable接口的實現類,並重寫它的Run方法,run方法同樣是該線程的執行體
- 創建Runnable實現類的實例,並將此實例作爲Thread的target創建一個Thread對象,
該Thread對象纔是真正的線程對象
- 調用start方法啓動該線程
public class Test {
public static void main(String[] args) {
System.out.println("主線程ID:"+Thread.currentThread().getId());
MyRunnable runnable = new MyRunnable();
Thread thread = new Thread(runnable);
thread.start();
}
}
class MyRunnable implements Runnable{
public MyRunnable() {
}
public void run() {
System.out.println("子線程ID:"+Thread.currentThread().getId());
}
}
這種方式必須將Runnable作爲Thread類的參數,然後通過Thread的start方法來創建一個新線程來執行該子任務。如果調用Runnable的run方法的話,是不會創建新線程的,這根普通的方法調用沒有任何區別
事實上,查看Thread類的實現源代碼會發現Thread類是實現了Runnable接口的
3.3 兩種繼承方式的區別
在Java中,這2種方式都可以用來創建線程去執行子任務,具體選擇哪一種方式要看自己的需求。直接繼承Thread類的話,可能比實現Runnable接口看起來更加簡潔,但是由於Java只允許單繼承,所以如果自定義類需要繼承其他類,則只能選擇實現Runnable接口。
- Thread是Runnable接口的子類,實現Runnable接口的方式解決了Java單繼承的侷限
- Runnable接口實現多線程比繼承Thread類更加能描述數據共享的概念
4 多線程有幾種實現同步方法 ?
同步的實現方面有兩種,分別是synchronized,wait與notify
- wait():使一個線程處於等待狀態,並且
釋放所持有的對象的lock
- sleep():使一個正在運行的線程處於睡眠狀態,是一個靜態方法,調用此方法要捕捉
InterruptedException
異常 - notify():喚醒一個處於等待狀態的線程,注意的是在調用此方法的時候,並不能確切的喚醒某一個等待狀態的線程,而是
由JVM確定喚醒哪個線程,而且不是按優先級
- Allnotity():喚醒所有處入等待狀態的線程,注意並不是給所有喚醒線程一個對象的鎖,而是
讓它們競爭
5 產生死鎖的原因
產生死鎖的四個必要條件:
- 1 互斥條件:一個資源每次只能被一個進程使用
- 2 請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放
- 3 不剝奪條件:進程已獲得的資源,在末使用完之前,不能強行剝奪
- 4 循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關係
6 避免死鎖
上面列出了死鎖的四個必要條件,我們只要想辦法破其中的任意一個或多個條件,就可以避免死鎖發生,一般有以下幾種方法
- 1 按同一順序訪問對象
- 2 避免事務中的用戶交互
- 3 保持事務簡短並處於一個批處理中
- 4 使用較低的隔離級別
- 5 使用基於行版本控制的隔離級別
- 6 使用綁定連接