什麼是執行緒安全,以及並發需要知道的幾個概念
- 2020 年 1 月 21 日
- 筆記
眾所周知,在Java的知識體系中,並發編程是非常重要的一環,也是面試的必問題,一個好的Java程式設計師是必須對並發編程這塊有所了解的。為了追求成為一個好的Java程式設計師,我決定從今天開始死磕Java的並發編程,盡量彌補自己在這方面的知識缺陷。
並發必須知道的概念
在深入學習並發編程之前,我們需要了解幾個基本的概念。
同步和非同步 同步和非同步用請求返回調用的方式來理解相對簡單。
同步:可以理解為發出一個請求後,必須等待返回結果才能執行下面的操作。
非同步:請求發出後,不需要等待返回結果,可以繼續執行後續操作,非同步請求更像是在另一個 「空間」 中處理請求的結果,這個過程不會影響請求方的其他操作。
舉個生活中的例子,比如我們去實體店買衣服,挑選完款式後下單讓售貨員去倉庫拿貨,在售貨員拿貨的過程你需要在店裡等待,直到售貨員把衣服交給你後才算購物成功,這就相當於同步的過程。
不過,如果是在網上購物的話,我們只需下單並完成支付,對我們來說整個購物過程就算完成了。網上的商家接到訂單會幫我們加緊安排送貨,這段時間我們可以去做其他的事,比如去外面打個籃球之類的。等送貨上門並簽收商品就完事了,這個過程就相當於非同步。
並發和並行 並發和並行的功能很相似,兩者都可以表示多個任務一起執行的情況,但本質上兩者其實是有區別的。
嚴格意義上來說,並行的多任務是真實的同時執行,而並發更多的情況是任務之間交替執行,系統不停的在多個任務間切換執行,也就是 「串列」 執行。
最直接的例子的就是我們的電腦系統,在單核CPU時代,系統表面上能同時進行多任務處理,比如聽歌的同時又瀏覽網頁,但真實環境中這些任務不可能是真實並行的,因為一個CPU一次只能執行一條指令,這種情況就是並發,系統看似能處理多任務是因為不停的切換任務,但因為時間非常短,所以在我們的感官來說就是同時進行的。而電腦系統真實的並行是隨著多核CPU的出現才有的。
臨界區 臨界區表示公共資源或是共享數據,可以被多個執行緒使用。但是每次只能有一個執行緒使用它,一旦臨界區的資源被佔用,其他執行緒就必須等到資源釋放後才能繼續使用該資源。在Java程式開發中,對於這樣的資源一般都需要做同步的操作,例如下面的這段程式碼,用的就是synchronized關鍵字來對臨界區資源進行同步。
public class SyncTest implements Runnable { //臨界區資源 public static SyncTest instance = new SyncTest(); @Override public void run() { synchronized (instance) { } } public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread(new SyncTest()); Thread t2 = new Thread(new SyncTest()); t1.start(); t2.start(); t1.join(); t2.join(); } }
阻塞和非阻塞 阻塞和非阻塞通常用來形容多執行緒間的相互影響。比如一個執行緒佔用了臨界區的資源,那麼其他需要這個資源的執行緒就必須等待。等待的過程會使執行緒掛起,也就是阻塞。如果臨界區的資源一直不釋放的話,那麼其他阻塞的執行緒就都不能工作了。
非阻塞則相反,強調的是執行緒之間並不互相妨礙,所有的執行緒都會不斷嘗試向前執行。
死鎖、飢餓和活鎖 這三種情況表示的是多執行緒間的活躍狀態,對於執行緒來說,以上的情況都是 「非友好」 的狀態。
1、死鎖一般是指兩個或者兩個以上的執行緒互相持有對方所需的資源,並且永遠在等待對方釋放的一種阻塞狀態。例如有兩個執行緒A和B同時共享臨界區的資源C,當A佔用C時,B處於阻塞狀態,然而A的釋放需要用到B的資源,這樣一來,就變成了A一直在等待B,B也一直在等待A,互相之間永遠在等待對方釋放的狀態。
一般來說,死鎖的發生是由於程式的設計不合理導致,而且死鎖很難解決,最好的方式就是預防。
2、飢餓是指某一個或者多個執行緒因為種種原因無法獲得所需的資源,導致一直無法執行。比如它的執行緒優先順序太低,而高優先順序的執行緒不斷搶佔它所需的資源,導致低優先順序資源無法工作。
3、活鎖的情況是執行緒一種非常有趣的情況,在生活中我們可能會碰到這樣的情況,那就是出門的時候可能會遇到有人要進門,你打算讓他先進門,他又打算讓你先出門,結果,兩個人都互相退後了,然後你打算先出門時對方也向前一步,來來回回就一直卡在門口。當然,這種事情正常人很快就能解決,但如果是執行緒碰到就沒那麼幸運了。
如果兩個執行緒佔用著公共的資源,並且秉承著 「謙讓」 的原則,主動把資源讓給他人使用,你讓我也讓,這樣就造成資源在兩個執行緒間不斷跳動但執行緒之間都拿不到資源的情況,這樣的情況就是活鎖了。
執行緒安全
執行緒安全指的是多執行緒的安全。如果一段程式可以保證被多執行緒訪問後仍能保持正確性,那麼程式就是執行緒安全的。一般來說,執行緒安全注重的是多執行緒開發中的共享數據的安全。就比如下面這段程式碼:
public class ThreadSafety implements Runnable{ //共享數據 public static int i = 0; public void increase(){ for (int j= 0;j<10; j++){ i++; } } @Override public void run() { increase(); } public static void main(String[] args) throws Exception{ ThreadSafety demo = new ThreadSafety(); Thread t1 = new Thread(); Thread t2 = new Thread(); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } }
兩個執行緒 t1 和 t2 同時開啟,執行run方法,在我們的預想中,如果是執行緒安全的話,那麼main的執行結果應該是20,但是因為 i 是共享數據,而程式沒有對 i 的操作做同步的處理,最終運行的結果並不是20,所以這種情況就不是執行緒安全的情況。
解決的辦法也比較簡單,可以利用synchronized關鍵字來修飾方法或程式碼塊,這部分的知識也是並發編程中非常重要的一塊,當然,本文就不探究了,之後單獨寫篇文章出來細說。