1. 程式人生 > 實用技巧 >《java併發》之1

《java併發》之1

上下文切換

CPU通過分配時間片來執行任務,當一個任務的時間片用完,就會切換到另一個任務。在切換之前會儲存上一個任務的狀態,當下次再切換到該任務,

就會載入這個狀態。——任務從儲存到再載入的過程就是一次上下文切換。

按導致上下文切換的因素劃分,可將上下文切換分為兩點:

  • 自發性上下文切換
  • 非自發性上下文切換

自發性上下文切換指執行緒由於自身因素導致的切出。通過呼叫下列方法會導致自發性上下文切換:

  • Thread.sleep()
  • Object.wait()
  • Thread.yeild()
  • Thread.join()
  • LockSupport.park()

非自發性上下文切換指執行緒由於執行緒排程器的原因被迫切出。發生下列情況可能導致非自發性上下文切換:


  • 切出執行緒的時間片用完
  • 有一個比切出執行緒優先順序更高的執行緒需要被執行
  • 虛擬機器的垃圾回收動作

上下文的切換流程

  • 掛起一個程序,將這個程序在CPU中的狀態(上下文資訊)儲存於 記憶體的PCB 中。
  • 在PCB中檢索下一個程序的上下文並將其在 CPU的暫存器 中恢復。
  • 跳轉到 程式計數器 所指向的位置,並恢復該程序。

程序切換為什麼比執行緒切換更消耗資源 ?

程序切換分兩步:

  • 切換頁目錄以使用新的地址空間
  • 切換核心棧和硬體上下文

對於linux來說,執行緒和程序的最大區別就在於地址空間

  • 對於執行緒切換,第1步是不需要做的,第2是程序和執行緒切換都要做的。
  • 執行緒切換虛擬記憶體空間依然是相同的,但是程序切換是不同的。

這兩種上下文切換的處理都是通過作業系統核心來完成的。核心的這種切換過程伴隨的最顯著的效能損耗是將暫存器中的內容切換出。

wait和sleep

  • 在等待時wait會釋放鎖,而sleep一直持有鎖
  • Wait通常被用於執行緒間互動,sleep通常被用於暫停執行

原理不同

sleep()方法是Thread類的靜態方法,是執行緒用來控制自身流程的,它會使執行緒暫停執行一段時間,而把執行機會讓給其他執行緒,等到計時時間一到,

此執行緒會自動被喚醒。

wait()方法是Object類的方法,用於執行緒間通訊,會是擁有該物件鎖的執行緒等待,直到其他執行緒呼叫notify()方法或者notifyAll()方法才會被

喚醒,

不過也可以給執行緒指定時間被喚醒。

對鎖的處理機制不同

由於sleep()方法的主要作用是讓執行緒暫停執行一段時間,時間一到則自動恢復,不涉及執行緒間通訊,因此呼叫sleep()方法並不會釋放鎖

wait方法則不同,當呼叫wait()方法後,執行緒會釋放掉它鎖佔用的鎖,從而使執行緒所在物件中的其他synchronized資料可被別的執行緒使用。

使用區域不同

wait()必須放在同步控制方法或者同步語句塊中使用,sleep()方法則可以放在任何地方使用


Java執行緒間具有優先順序,在呼叫棧可以設定優先級別(1-10)


yield()讓當前執行執行緒回到可執行狀態,以允許具有相同優先順序的其他執行緒獲得執行機會。

但是,實際中無法保證yield()達到讓步目的,因為讓步的執行緒還有可能被執行緒排程程式再次選中。


join()方法:join()方法可以使得一個執行緒在另一個執行緒結束後再執行。線上程B中呼叫了執行緒A的
Join()方法,直到執行緒A執行完畢後,才會繼續執行執行緒B。


多執行緒有幾種建立方式

  • 繼承Thread類,重寫run方法
  • 實現Runnable介面,重寫run方法
  • 通過Callable和FutureTask建立執行緒
  • 通過執行緒池建立執行緒

Runnable和Callable的區別
相同點

  • 兩者都是介面;
  • 兩者都可用來編寫多執行緒程式;
  • 兩者都需要呼叫Thread.start()啟動執行緒;

不同點

實現Callable介面的任務執行緒返回執行結果;而實現Runnable介面的任務執行緒不能返回結果;

Callable介面的call()方法允許丟擲異常;而Runnable介面的run()方法的異常只能在內部消化,不能繼續上拋;

Callable<String> callable = new CallableImpl("my callable test!");
FutureTask<String> task = new FutureTask<>(callable);
// 建立執行緒
new Thread(task).start();
// 呼叫get()阻塞主執行緒,反之,執行緒不會阻塞
String result = task.get();

注意點
Callable介面支援返回執行結果,需要呼叫FutureTask.get()方法實現,此方法會阻塞主執行緒直到獲取結果;當不呼叫此方法時,主執行緒不會阻塞


ExcutorService中的execute和submit方法的區別
兩者都是將一個執行緒任務新增到執行緒池中並執行;
1、execute沒有返回值,submit有返回值,並且返回執行結果Future物件
2、execute不能提交Callable任務,只能提交Runnable任務,submit兩者任務都可以提交
3、在submit中提交Runnable任務,會返回執行結果Future物件,但是Future呼叫get方法將返回null
(Runnable沒有返回值)

FutureTask
FutureTask實現了RunnableFuture,RunnableFuture既實現了Runnbale又實現了Futrue這兩個介面;
它兩者的結合體
FutureTask又包裝了Callable(如果是Runnable最終會轉化為Callable);
FutureTask可以通過Thread包裝來直接執行,也可以提交給ExcutorService來執行,
並可以直接通過get方法來獲取執行結果;

首先說下多執行緒出現的原因:

為了解決負載均衡問題,充分利用CPU資源.為了提高CPU的使用率,
為了處理大量的IO操作時或處理的情況需要花費大量的時間等等,比如:讀寫檔案

因為處理一條請求,經常涉及到RPC、資料庫訪問、磁碟IO等操作,這些操作的速度比CPU慢很多,
而在等待這些響應的時候,CPU卻不能去處理新的請求

多執行緒技術使程式的響應速度更快


多執行緒的缺點:

  • 如果有大量的執行緒,會影響效能,因為作業系統需要在它們之間切換.
  • 更多的執行緒需要更多的記憶體空間
  • 防止執行緒死鎖情況的發生


當我們呼叫shutdown()方法後,執行緒池會等待我們已經提交的任務執行完成。
但是此時執行緒池不再接受新的任務,如果我們再向執行緒池中提交任務,將會拋RejectedExecutionException異常。
如果執行緒池的shutdown()方法已經呼叫過,重複呼叫沒有額外效應。注意,當我們呼叫shutdown()方法
後,會立即從該方法中返回,而不會阻塞等待執行緒池關閉再返回,如果希望阻塞等待可以呼叫awaitTermination()方法。


與shutdown()方法不同的是shutdownNow()方法呼叫後,執行緒池會通過呼叫worker執行緒的interrupt方法
盡最大努力(best-effort)去"終止"已經執行的任務。
而對於那些在堵塞佇列中等待執行的任務,執行緒池並不會再去執行這些任務,而是直接返回這些等待
執行的任務,也就是該方法的返回值。


值得注意的是,當我們呼叫一個執行緒的interrupt()方法後(前提是caller執行緒有許可權,否則拋異常),該執行緒並不一定會立馬退出:

如果執行緒處於被阻塞狀態(例如處於sleep, wait, join 等狀態),那麼執行緒立即退出被阻塞狀態,
並丟擲一個InterruptedException異常。
如果執行緒處於正常的工作狀態,該方法只會設定執行緒的一個狀態位為true而已,執行緒會繼續執行不受影響。
如果想停止執行緒執行可以在任務中檢查當前執行緒的狀態(Thread.isInterrupted())自己實現停止邏輯。


執行緒池的選擇策略
可以從以下角度分析:
①任務性質:CPU 密集型、IO 密集型和混合型。
②任務優先順序。
③任務執行時間。
④任務依賴性:是否依賴其他資源,如資料庫連線。

性質不同的任務可用不同規模的執行緒池處理,
CPU 密集型任務應配置儘可能小的執行緒,如配置 Ncpu+1 個執行緒的執行緒池。
由於 IO 密集型任務執行緒並不是一直在執行任務,應配置儘可能多的執行緒,如 2*Ncpu。
混合型的任務,如果可以拆分,將其拆分為一個 CPU 密集型任務和一個 IO 密集型任務,
只要兩個任務執行的時間相差不大那麼分解後的吞吐量將高於序列執行的吞吐量,
如果相差太大則沒必要分解。

優先順序不同的任務可以使用優先順序佇列 PriorityBlockingQueue 處理。

執行時間不同的任務可以交給不同規模的執行緒池處理,或者使用優先順序佇列讓執行時間短的任務先執行。

依賴資料庫連線池的任務,由於執行緒提交 SQL 後需要等待資料庫返回的結果,等待的時間越長 CPU
空閒的時間就越長,因此執行緒數應該儘可能地設定大一些,提高 CPU 的利用率。
建議使用有界佇列,能增加系統的穩定性和預警能力,可以根據需要設定的稍微大一些。


stop會導致不安全,為啥呢,如果在同步塊執行一半時,stop來了,後面還沒執行完呢,鎖沒了,
執行緒退出了,別的執行緒又可以操作你的資料了,所以就是執行緒不安全了。

suspend會導致死鎖,因為掛起後,是不釋放鎖的,別人也就阻塞著,如果沒人喚醒,那就一直死鎖。