1. 程式人生 > >面試必備——Java多執行緒與併發(一)

面試必備——Java多執行緒與併發(一)

1.程序和執行緒的

(1)由來

1)序列

最初的計算機只能接受一些特定的指令,使用者輸入一個指令,計算機就做出一個操作。當用戶在思考或者輸入時,計算機就在等待。顯然這樣效率低下,在很多時候,計算機都處在等待狀態。

2)批處理

提高計算機的效率,不用等待使用者的輸入,把一系列需要操作的指令寫下來,形成一個清單,一次性交給計算機,計算機不斷讀取指令進行相應的操作,就這樣,批處理作業系統誕生了。使用者將多個需要執行的程式寫在磁帶上,然後交由計算機去讀取並逐個執行這些程式,並將輸出結果寫在另一個磁帶上。
存在問題:
當有兩個任務A和B,任務A執行到一半的過程中,需要讀取大量的資料輸入(I/O操作),而此時CPU只能等任務A讀取完資料再能繼續進行,這樣就白白浪費了CPU資源。於是人們就想,能否在任務A讀取資料的過程中,讓任務B去執行,當任務A讀取完資料之後,暫停任務B,讓任務A繼續執行? 這時候又出現了幾個問題:記憶體中始終都只有一個程式在執行,而想要解決上述問題,必然要在記憶體中裝入多個程式,如何處理呢?多個程式使用的資料如何辨別?當一個程式暫停後,隨後怎麼恢復到它之前執行的狀態呢?此時,程序應運而生.

3)程序

用程序來對應一個程式,每個程序來對應一定的記憶體地址空間,並且只能使用它自己的記憶體空間,各個程序之間互不干擾。儲存了程式每個時刻的執行狀態,為程序切換提供了可能。當程序暫停時,它會儲存當前程序的狀態(程序標識,程序使用的資源等),在下一次切換回來時根據之前儲存的狀態進行恢復,接著繼續執行。程序讓操作體統的併發成為了可能。雖然併發從巨集觀上看有多個任務在執行,但事實上,對於單核CPU來說,任意具體時刻都只有一個任務在佔用CPU資源。之所以造成使用者在巨集觀上的假象,CPU分配給單一任務的時間片很短,切換頻次高。 程序出現之後,作業系統的效能得到了大大的提升。雖然解決了作業系統的併發問題,隨著計算機的發展,人們並不滿足這樣,逐漸對實時性有了要求。因為一個程序在一個時間段內只能做一個事情,如果一個程序有多個子任務時,只能逐個得執行這些子任務,子任務之間往往不存在順序上的依賴,是可以併發執行的,既然CPU可以按照時間片的方式輪流切換程序,能不能給子任務打上標籤,按照更細的時間片去執行?

4)執行緒

答案是肯定的,由於子任務共享程序的記憶體等資源,相互間切換更快速,人們發明了執行緒,讓一個執行緒執行一個子任務,這樣一個程序就包含了多個執行緒,每個執行緒負責一個單獨的子任務。 程序讓作業系統的併發性成為了可能,而執行緒讓程序的內部併發成為了可能。

(2)區別

程序是資源分配的最小單位(程序之間互不干擾),執行緒是CPU排程的最小單位(執行緒間互相切換)。
  • 所有與程序相關的資源,都被記錄在PCB(程序控制塊)中
  • 程序是搶佔處理器的排程單位;執行緒屬於某個程序,共享其資源

  • 執行緒只由堆疊暫存器、程式計數器和TCB組成

總結

  • 執行緒不能看做獨立應用,而程序可看做獨立應用
  • 程序有獨立的地址空間,相互不影響,執行緒只是程序的不同執行路徑
  • 程序資料分開,共享複雜,同步簡單;執行緒共享簡單,同步複雜
  • 執行緒沒有獨立的地址空間,多程序的程式比多執行緒程式健壯(程序出現問題不會影響其他程序,可靠高;一個執行緒掛掉,整個程序也會掛掉,可靠低)
  • 程序的切換比執行緒的切換開銷大(程序單獨佔有一定的記憶體地址空間,程序的建立和銷燬不僅需要儲存暫存器和棧資訊,還需要資源的分配回收以及頁排程,開銷較大;執行緒只需要儲存暫存器和棧資訊,開銷較小)

(3)JAVA程序和執行緒的關係

  • Java對作業系統提供的功能進行封裝,包括程序和執行緒
  • 執行一個程式會產生一個程序,程序包含至少一個執行緒
  • 每個程序對應一個JVM例項,多個執行緒共享JVM裡的堆,JVM是多執行緒的
  • Java採用單執行緒程式設計模型,程式會自動建立主執行緒
  • 主執行緒可以建立子執行緒,原則上要後於子執行緒完成執行,因為要執行各種關閉動作

2.執行緒的start和run的區別

  • 呼叫start()方法會建立一個新的子執行緒並啟動
  • run()方法只是Thread的一個普通方法的呼叫

3.Thread和Runnable是什麼關係

  • Thread是實現了Runnable介面的類,通過Thread的start()方法可以給Runnable的run()方法附上多執行緒的特性
  • 因Java類的單一繼承原則,推薦多使用Runnable介面(為了提升系統的可拓展性,通過使業務類實現Runnable介面,將業務邏輯封裝在run方法裡,後續可以給普通類附上多執行緒的特性)

4.如何實現處理執行緒的返回值

(1)主執行緒等待法:實現簡單,我們可以通過實現迴圈等待的邏輯;缺點是變數多的時候會顯得臃腫,無法精準控制

 1 public class CycleWait implements Runnable {
 2  
 3   private String value;
 4  
 5   @Override
 6   public void run() {
 7     try {
 8       Thread.currentThread().sleep(5000);
 9     } catch (InterruptedException e) {
10       e.printStackTrace();
11     }
12     value = "we have date now";
13   }
14   
15   public static void main(String[] args) throws InterruptedException {
16     CycleWait cw = new CycleWait();
17     Thread t = new Thread(cw);
18     t.start();
19     //當值為null的時候一直迴圈,直到有值時才退出迴圈
20 
21     while (cw.value == null) {
22 
23         Thread.currentThread().sleep(100);
24 
25     }
26     System.out.println("value:" + cw.value); // 沒有前面的迴圈,可能取出的值為null
27   }
28 }
View Code

(2)使用Thread類的join()阻塞當前執行緒以等待子執行緒處理完畢:實現更簡單,缺點是力度不夠細,無法精確控制

1 public static void main(String[] args) throws InterruptedException {
2     CycleWait cw = new CycleWait();
3     Thread t = new Thread(cw);
4     t.start();
5     t.join();
6     System.out.println("value:" + cw.value); 
7   }
View Code

(3)通過Callable介面實現:JDK5.0新增的,具體可以通過FutureTask或執行緒池獲取

 1 public class myCallable implements Callable {
 2   @Override
 3   public String call() throws Exception {
 4     String value = "test";
 5     System.out.println("Ready to work");
 6     Thread.currentThread().sleep(3000);
 7     System.out.println("task done");
 8     return value;
 9   }  
10 }
View Code
  • FutureTask
1 public static void main(String[] args) throws ExecutionException, InterruptedException {
2    FutureTask<String> ft = new FutureTask<String>(new myCallable());
3    new Thread(ft).start();
4    if (!ft.isDone()) {
5       System.out.println("task has not finished, please wait!");
6    }
7    System.out.println("task reture:" + ft.get());
8 }
View Code
  • 執行緒池
 1 public static void main(String[] args) {
 2    ExecutorService executorService = Executors.newCachedThreadPool();
 3    Future<String> future = executorService.submit(new myCallable());
 4    if (!future.isDone()) {
 5       System.out.println("task has not finished, please wait!");
 6    }
 7    try {
 8       System.out.println("task reture:" + future.get());
 9    } catch (InterruptedException e) {
10       e.printStackTrace();
11    } catch (ExecutionException e) {
12       e.printStackTrace();
13    } finally {
14       executorService.shutdown();
15    }
16 }
View Code

執行結果

1 task has not finished, please wait!
2 Ready to work
3 task done
4 task reture:test

5.執行緒的狀態(6個)

(1)新建(New):建立後尚未啟動的執行緒的狀態

(2)執行(Runnable):包含Running和Ready(Running:正在執行;Ready:等待CPU分配執行時間)

(3)無限期等待(Waiting):不會被分配CPU執行時間,需要顯示被喚醒

  • 沒有設定Timeout引數的Object.wait()方法
  • 沒有設定Timeout引數的Thread.join()方法
  • LockSupport.park()方法

(4)限期等待(Timed Waiting):在一定時間後由系統自動喚醒

  • Thread.sleep()方法
  • 設定了Timeout引數的Object.wait()方法
  • 設定了Timeout引數的Thread.join()方法
  • LockSupport.parkNanos()方法
  • LockSupport.parkUntil()方法

(5)阻塞(Blocked):等待獲取排它鎖

(6)結束(Terminated):已終止執行緒的狀態,執行緒已經結束執行

6.sleep和wait的區別

(1)基本差別

  • sleep是Thread類的方法,wait是Object類中定義的方法
  • sleep()方法可以在任何地方使用
  • wait()方法只能在synchronized方法或synchronized塊中使用

(2)最主要的本質區別

  • Thread.sleep只會讓出CPU,不會釋放鎖
  • Object.wait不僅會讓出CPU,還會釋放鎖(這個方法要寫在synchronized裡面,因為要獲得鎖,才能釋放鎖)

 7.notify和notifyall的區別

(1)需要先了解的兩個概念

  • 鎖池EntryList
假設執行緒A已經擁有了某個物件(不是類)的鎖,而其他執行緒B、C想要呼叫這個物件的某個synchronized方法(或者塊)之前必須先獲得該物件鎖的擁有權,而恰巧該物件的鎖目前正被執行緒A所佔用,此時B、C執行緒就會被阻塞,進入一個地方去等待鎖的釋放,這個地方便是該物件的鎖池
  • 等待池WaitSet
假設執行緒A呼叫了某個物件的wait()方法,執行緒A就會釋放該物件的鎖,同時執行緒A就進入到了該物件的等待鎖中,進入到等待池中的執行緒不會去競爭該物件的鎖

(2)區別

  • notifyAll會讓所有處於等待池的執行緒全部進入鎖池去競爭獲取鎖的機會
  • notify只會隨機選取一個處於等待池中的執行緒進入鎖池去競爭獲取鎖的機會

8.yield相關

當呼叫Thread.yield()函式時,會給執行緒排程器一個當前執行緒願意讓出CPU使用的暗示,但是執行緒排程器可能會忽略這個暗示,該方法不釋放鎖。

 1 public static void main(String[] args) {
 2    Runnable yieldTask = () -> {
 3       for (int i = 0; i <= 10; i++) {
 4          System.out.println(Thread.currentThread().getName() + i);
 5          if (i == 5){
 6             Thread.yield(); // 暗示執行緒排程器願意讓出CPU使用,但最終決定權還是線上程排程器中
 7          }
 8       }
 9    };
10    Thread thread1 = new Thread(yieldTask,"A");
11    Thread thread2 = new Thread(yieldTask,"B");
12    thread1.start();
13    thread2.start();
14 }
View Code

9.interrupt相關

(1)設計理念

一個執行緒不應該由其他執行緒來強制中斷或停止,而是應該由執行緒自己自行停止,是一種比較溫柔的做法,它更類似一個標誌位。其作用不是中斷執行緒,而是通知執行緒應該中斷了,具體到底中斷還是繼續執行,應該由被通知的執行緒自己處理。

(2)如何中斷執行緒

1)已經被拋棄的方法

  • 通過呼叫stop()方法停止執行緒
它可以是由一個執行緒去停止另外一個執行緒,太過暴力,而且不安全。 比如說,執行緒A去停止執行緒B,但是不知道執行緒B執行的情況,突然停止,會導致執行緒B的清理工作無法完成,還有其他情況,執行stop方法後,執行緒B會馬上釋放鎖,可能引發資料不同步問題
  • 通過呼叫suspend()和resume()方法

2)目前使用的方法

呼叫interrupt(),通知執行緒應該中斷了
  • 如果執行緒處於被阻塞狀態,那麼執行緒立即退出被阻塞狀態,並丟擲一個InterruptedException異常
  • 如果執行緒處於正常活動狀態,那麼會將該執行緒的中斷標誌設定為true。被設定中斷標誌的執行緒將繼續正常執行,不受影響
因此,interrupt並不能真正中斷執行緒,需要被呼叫的執行緒配合去中斷
  • 在正常執行任務時,經常檢查本執行緒的中斷標誌位,如果被設定了中斷標誌就自行停止執行緒
  • 呼叫Thread.interrupted() 方法後執行緒恢復非中斷狀態,即Thread.currentThread().isInterrupted()是false