面試必備——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
Tags: