做者:湯圓java
我的博客:javalover.cc編程
前言
官人們好啊,我是湯圓,今天給你們帶來的是《線程的安全性 - 併發基礎篇》,但願有所幫助,謝謝安全
文章純屬原創,我的總結不免有差錯,若是有,麻煩在評論區回覆或後臺私信,謝啦網絡
簡介
當多個線程訪問某個類時,這個類始終都能表現出正確的行爲,那麼就說這個類是線程安全的多線程
目錄
此次分三步走:關於相關知識點,放在文末的腦圖裏了,你們想看結論的,可直接下拉觀看哦併發
- 建立一個線程安全的類
- 建立一個線程不安全的類:有一個狀態變量
- 建立一個線程不安全的類:有多個狀態變量
正文
線程的安全性主要是針對對象的狀態(實例屬性或靜態屬性)而言的,若是在多線程中,訪問到的對象狀態不一致(好比常見的自增屬性),那麼就是線程不安全的高併發
下面咱們一步步來性能
先來個無狀態類url
第一步:無狀態類
這裏咱們寫一個簡單的線程安全類,簡單到什麼地步呢?以下所示.net
public class SafeDemo { public int sum(int n, int m){ return n + m; } }
就是這麼簡單,咱們說這個類是線程安全的
爲啥安全呢?
由於這個類沒有狀態,即無狀態類;
只有局部變量n,m,而這些局部變量是存在於棧中的,棧是每一個線程獨有的,不跟其餘線程共享,堆才共享
因此每一個線程操做sum時,對應的n,m只有本身可見,固然就安全了
好了,經過上面的例子,咱們知道了什麼是線程安全類,那本節的內容就到此結束了,再見
上面的例子,咱們舉了一個無狀態類,接下來咱們添加一個狀態試試
第二步:加一個狀態變量
加一個狀態變量(靜態屬性),代碼以下
public class UnSafeDemo { static int a = 0; public static void main(String[] args) throws InterruptedException { // 線程1 new Thread(()-> { for(int j=0;j<100000;j++){ a++; } }).start(); // 線程2 new Thread(()-> { for(int j=0;j<100000;j++){ a++; } }).start(); Thread.sleep(3000); // 這裏不是每次運行都會輸出200,000 System.out.println(a); } }
上面咱們建立了兩個線程,每一個線程都執行10萬次的自增操做
可是由於自增不是原子操做,實際分三步:讀-改-寫
此時若是兩個線程同時讀到相同的值,則累加次數就會少一次
這種在併發編程中,因爲不恰當的執行時序而出現不正確的結果的狀況,叫作競態條件
以下圖所示:
指望的是正常執行,每一個線程交替執行
結果卻有多是不正常的,以下
這時咱們就能夠說,上面加的這個狀態是不安全的,結果就是整個類也是不安全的
不安全的狀態有二:
-
可變狀態(變量):非final修飾的變量
-
共享狀態(變量):非局部變量
像上面這個例子,狀態就同時屬於可變狀態和共享狀態
那要怎麼確保安全:
-
同步:synchronized、volatile、顯式鎖、原子變量(好比AtomicInteger)
-
不可變變量:final(都不能改了,固然安全了)
-
不共享變量:不在多線程中共享變量(即局部變量)
PS:代碼的封裝性越好,訪問可變變量的代碼塊越少,越容易確保線程安全
這裏的自增咱們就能夠用同步中的原子變量來解決
關於原子變量的細節,後面章節再介紹,這裏只須要知道,原子變量內部的操做是原子操做就能夠了
修改後的代碼以下:
public class SafeDemo { static final AtomicInteger a = new AtomicInteger(0); // static int a = 0; public static void main(String[] args) throws InterruptedException { // 線程1 new Thread(()-> { for(int j=0;j<100000;j++){ // 這裏的自增是原子操做 a.incrementAndGet(); } }).start(); // 線程2 new Thread(()-> { for(int j=0;j<100000;j++){ // 這裏的自增是原子操做 a.incrementAndGet(); } }).start(); Thread.sleep(3000); System.out.println(a.get()); } }
能夠看到,加了AtomicInteger.incrementAndGet()方法,這個方法是原子操做
這時,無論怎麼運行,都是輸出200,000
第三步:加多個狀態變量
上面咱們加了一個狀態變量,能夠用原子變量來保證線程安全
那若是是多個狀態變量呢?此時就算用了原子變量也不行了
由於原子變量只是保證它內部是原子操做,可是當多個原子變量放到一塊兒組合操做時,他們之間又存在競態條件了,就又不是原子操做了
競態條件:併發編程中,因爲不恰當的執行時序而出現不正確的結果的狀況,就是競態條件(重複陳述ing,加深記憶)
代碼以下:
public class UnSafeDemo2 { static final AtomicInteger a = new AtomicInteger(0); static final AtomicInteger b = new AtomicInteger(0); public static void main(String[] args) throws InterruptedException { new Thread(()-> { for(int j=0;j<10000;j++){ a.incrementAndGet(); b.incrementAndGet(); if(a.get()!=b.get()){ // 理想狀態的話,不會運行到這裏,由於a和b是一塊兒自增的 // 可是大部分時候都是不正常的,由於a和b各自是原子操做,可是放到一塊兒就不是原子操做了 System.out.println(1); } } }).start(); new Thread(()-> { for(int j=0;j<10000;j++){ a.incrementAndGet(); b.incrementAndGet(); if(a.get()!=b.get()){ // 理想狀態的話,不會運行到這裏,由於a和b是一塊兒自增的 // 可是大部分時候都是不正常的,由於a和b各自是原子操做,可是放到一塊兒就不是原子操做了 System.out.println(2); } } }).start(); } }
上面屢次運行,會發現基本上每次都會打印1和2,就是由於這兩個線程之間存在競態條件
那怎麼解決呢?
上鎖
代碼以下:
public class UnSafeDemo2 { static final AtomicInteger a = new AtomicInteger(0); static final AtomicInteger b = new AtomicInteger(0); public static void main(String[] args) throws InterruptedException { // 單首創建一個對象,用來充當鎖 UnSafeDemo2 unSafeDemo2 = new UnSafeDemo2(); new Thread(()-> { for(int j=0;j<10000;j++){ // 這裏加了鎖 synchronized (unSafeDemo2){ a.incrementAndGet(); b.incrementAndGet(); if(a.get()!=b.get()){ // 如今確定是理想狀態,不會運行到這裏 System.out.println(1); } } } }).start(); new Thread(()-> { for(int j=0;j<10000;j++){ // 這裏加了鎖 synchronized (unSafeDemo2){ a.incrementAndGet(); b.incrementAndGet(); if(a.get()!=b.get()){ // 如今確定是理想狀態,不會運行到這裏 System.out.println(2); } } } }).start(); } }
這裏用到的鎖爲內置鎖,還有不少其餘鎖,這裏就不展開了(後面章節再介紹)
這裏要注意:同步代碼必須上同一個鎖纔有用,好比上面的例子,兩個線程都是上的unsafeDemo2這個鎖
官人們能夠試一下,一個上unsafeDemo2鎖,一個上Object鎖,看會輸出啥
內置鎖也叫監視器鎖
特色:
-
互斥性:即一個線程持有鎖,其餘線程就要等待鎖釋放後才能夠獲取鎖
-
可重入性:若是某個線程嘗試去獲取一個鎖,而這個鎖以前就是這個線程所持有的,那麼這個線程就能夠再次獲取到鎖
-
好處:
- 避免了死鎖:好比一個子類繼承父類的synchronized方法,並顯示調用父類的synchronized方法,若是不可重入,那麼在子類中獲取的鎖,調用子類的fun方法是沒問題的,可是調用父類的fun方法時,會提示上了鎖,從而被阻塞,此時就會死鎖(本身持有鎖,還有再去獲取鎖,可是又獲取不到)
-
缺點:
- 跟狀態有關的方法都須要上鎖:操做麻煩,其實就是類的每一個方法都須要上鎖,若是後面添加了一個方法,忘記加鎖,那仍是有安全問題(好比被官人們遺棄的Vector)
- 性能問題:整個方法都上鎖,性能很低,尤爲是一些耗時操做,好比網絡IO這種容易阻塞的操做
-
解決:
- 縮小鎖的範圍
- 將耗時長的操做(前提是操做與狀態無關),放到同步以外的代碼塊
-
好了,差很少先這些吧,後面還有太多東西了,慢慢來吧。
畢竟咱們都一大把年紀了,身體要緊吶。
總結
懶了懶了,直接貼圖了(敲的腦仁疼),圖作的不是很好,不過應該能看懂,望見諒哈
參考內容:
- 《Java併發編程實戰》
- 《實戰Java高併發》
後記
最後,感謝你們的觀看,謝謝
原創不易,期待官人們的三連喲