1. 程式人生 > >讓你秒懂執行緒和執行緒安全,只需5步!

讓你秒懂執行緒和執行緒安全,只需5步!

在探討執行緒安全之前,我們先來聊聊什麼是程序。

什麼是程序?

電腦中時會有很多單獨執行的程式,每個程式有一個獨立的程序,而程序之間是相互獨立存在的。比如下圖中的QQ、酷狗播放器、電腦管家等等。

什麼是執行緒?

程序想要執行任務就需要依賴執行緒。換句話說,就是程序中的最小執行單位就是執行緒,並且一個程序中至少有一個執行緒。

那什麼是多執行緒?提到多執行緒這裡要說兩個概念,就是序列和並行,搞清楚這個,我們才能更好地理解多執行緒。

所謂序列,其實是相對於單條執行緒來執行多個任務來說的,我們就拿下載檔案來舉個例子:當我們下載多個檔案時,在序列中它是按照一定的順序去進行下載的,也就是說,必須等下載完A之後才能開始下載B,它們在時間上是不可能發生重疊的。

並行:下載多個檔案,開啟多條執行緒,多個檔案同時進行下載,這裡是嚴格意義上的,在同一時刻發生的,並行在時間上是重疊的。

瞭解了這兩個概念之後,我們再來說說什麼是多執行緒。舉個例子,我們開啟騰訊管家,騰訊管家本身就是一個程式,也就是說它就是一個程序,它裡面有很多的功能,我們可以看下圖,能查殺病毒、清理垃圾、電腦加速等眾多功能。

按照單執行緒來說,無論你想要清理垃圾、還是要病毒查殺,那麼你必須先做完其中的一件事,才能做下一件事,這裡面是有一個執行順序的。

如果是多執行緒的話,我們其實在清理垃圾的時候,還可以進行查殺病毒、電腦加速等等其他的操作,這個是嚴格意義上的同一時刻發生的,沒有執行上的先後順序。

以上就是,一個程序執行時產生了多個執行緒。

在瞭解完這個問題後,我們又需要去了解一個使用多執行緒不得不考慮的問題——執行緒安全。

今天我們不說如何保證一個執行緒的安全,我們聊聊什麼是執行緒安全?因為我之前面試被問到了,說真的,我之前真的不是特別瞭解這個問題,我們好像只學瞭如何確保一個執行緒安全,卻不知道所謂的安全到底是什麼!

什麼是執行緒安全?

既然是執行緒安全問題,那麼毫無疑問,所有的隱患都是在多個執行緒訪問的情況下產生的,也就是我們要確保在多條執行緒訪問的時候,我們的程式還能按照我們預期的行為去執行,我們看一下下面的程式碼。

很簡單的一段程式碼,下面我們就來統計一下這個方法的訪問次數,多個執行緒同時訪問會不會出現什麼問題,我開啟的3條執行緒,每個執行緒迴圈10次,得到以下結果:

我們可以看到,這裡出現了兩個26,出現這種情況顯然表明這個方法根本就不是執行緒安全的,出現這種問題的原因有很多。

最常見的一種,就是我們A執行緒在進入方法後,拿到了count的值,剛把這個值讀取出來,還沒有改變count的值的時候,結果執行緒B也進來的,那麼導致執行緒A和執行緒B拿到的count值是一樣的。

那麼由此我們可以瞭解到,這確實不是一個執行緒安全的類,因為他們都需要操作這個共享的變數。其實要對執行緒安全問題給出一個明確的定義,還是蠻複雜的,我們根據我們這個程式來總結下什麼是執行緒安全。

當多個執行緒訪問某個方法時,不管你通過怎樣的呼叫方式、或者說這些執行緒如何交替地執行,我們在主程式中不需要去做任何的同步,這個類的結果行為都是我們設想的正確行為,那麼我們就可以說這個類是執行緒安全的。  

搞清楚了什麼是執行緒安全,接下來我們看看Java中確保執行緒安全最常用的兩種方式。先來看段程式碼。

大家覺得這段程式碼是執行緒安全的嗎?

毫無疑問,它絕對是執行緒安全的,我們來分析一下,為什麼它是執行緒安全的?

我們可以看到這段程式碼是沒有任何狀態的,就是說我們這段程式碼,不包含任何的作用域,也沒有去引用其他類中的域進行引用,它所執行的作用範圍與執行結果只存在它這條執行緒的區域性變數中,並且只能由正在執行的執行緒進行訪問。當前執行緒的訪問,不會對另一個訪問同一個方法的執行緒造成任何的影響。

兩個執行緒同時訪問這個方法,因為沒有共享的資料,所以他們之間的行為,並不會影響其他執行緒的操作和結果,所以說無狀態的物件,也是執行緒安全的。

新增一個狀態呢?

如果我們給這段程式碼新增一個狀態,新增一個count,來記錄這個方法並命中的次數,每請求一次count+1,那麼這個時候這個執行緒還是安全的嗎?

很明顯已經不是了,單執行緒執行起來確實是沒有任何問題的,但是當出現多條執行緒併發訪問這個方法的時候,問題就出現了,我們先來分析下count+1這個操作。

進入這個方法之後首先要讀取count的值,然後修改count的值,最後才把這把值賦值給count,總共包含了三步過程:“讀取”一>“修改”一>“賦值”,既然這個過程是分步的,那麼我們先來看下面這張圖,看看你能不能看出問題:

可以發現,count的值並不是正確的結果,當執行緒A讀取到count的值,但是還沒有進行修改的時候,執行緒B已經進來了,然後執行緒B讀取到的還是count為1的值,正因為如此所以我們的count值已經出現了偏差,那麼這樣的程式放在我們的程式碼中,是存在很多的隱患的。

如何確保執行緒安全?

既然存線上程安全的問題,那麼肯定得想辦法解決這個問題,怎麼解決?我們說說常見的幾種方式。

1、synchronized

synchronized關鍵字,就是用來控制執行緒同步的,保證我們的執行緒在多執行緒環境下,不被多個執行緒同時執行,確保我們資料的完整性,使用方法一般是加在方法上。

這樣就可以確保我們的執行緒同步了,同時這裡需要注意一個大家平時忽略的問題,首先synchronized鎖的是括號裡的物件,而不是程式碼,其次,對於非靜態的synchronized方法,鎖的是物件本身也就是this。

當synchronized鎖住一個物件之後,別的執行緒如果想要獲取鎖物件,那麼就必須等這個執行緒執行完釋放鎖物件之後才可以,否則一直處於等待狀態。

注意點:雖然加synchronized關鍵字,可以讓我們的執行緒變得安全,但是我們在用的時候,也要注意縮小synchronized的使用範圍,如果隨意使用時很影響程式的效能,別的物件想拿到鎖,結果你沒用鎖還一直把鎖佔用,這樣就有點浪費資源。

2、Lock

先來說說它跟synchronized有什麼區別吧,Lock是在Java1.6被引入進來的,Lock的引入讓鎖有了可操作性,什麼意思?就是我們在需要的時候去手動的獲取鎖和釋放鎖,甚至我們還可以中斷獲取以及超時獲取的同步特性,但是從使用上說Lock明顯沒有synchronized使用起來方便快捷。我們先來看下一般是如何使用的:

進入方法我們首先要獲取到鎖,然後去執行我們業務程式碼,這裡跟synchronized不同的是,Lock獲取的所物件需要我們親自去進行釋放,為了防止我們程式碼出現異常,所以我們的釋放鎖操作放在finally中,因為finally中的程式碼無論如何都是會執行的。

寫個主方法,開啟兩個執行緒測試一下我們的程式是否正常:

結果:

可以看出我們的執行,是沒有任何問題的。

其實在Lock還有幾種獲取鎖的方式,我們這裡再說一種,就是tryLock()這個方法跟Lock()是有區別的,Lock在獲取鎖的時候,如果拿不到鎖,就一直處於等待狀態,直到拿到鎖,但是tryLock()卻不是這樣的,tryLock是有一個Boolean的返回值的,如果沒有拿到鎖,直接返回false,停止等待,它不會像Lock()那樣去一直等待獲取鎖。

我們來看下程式碼:

結果:我們繼續使用剛才的兩個執行緒進行測試可以發現,線上程t1獲取到鎖之後,執行緒t2立馬進來,然後發現鎖已經被佔用,那麼這個時候它也不在繼續等待。

似乎這種方法,感覺不是很完美,如果我第一個執行緒,拿到鎖的時間,比第二個執行緒進來的時間還要長,是不是也拿不到鎖物件?

那我能不能,用一中方式來控制一下,讓後面等待的執行緒,可以等待5秒,如果5秒之後,還獲取不到鎖,那麼就停止等,其實tryLock()是可以進行設定等待的相應時間的。

結果:看上面的程式碼,我們可以發現,雖然我們獲取鎖物件的時候,可以等待2秒,但是我們執行緒t1在獲取鎖物件之後,執行任務缺花費了3秒,那麼這個時候執行緒t2是不在等待的。

我們再來改一下這個等待時間,改為5秒,再來看下結果:

結果:這個時候我們可以看到,執行緒t2等到5秒獲取到了鎖物件,執行了任務程式碼。

以上就是使用Lock,來保證我們執行緒安全的方式。