線程的創建和多線程

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 使用綁定連接
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章