5個步驟,教你瞬間明白線程和線程安全

2020-12-13 CSDN

作者 | 一個程式設計師的成長

責編 | 胡巍巍

記得今年3月份剛來杭州面試的時候,有一家公司的技術總監問了我這樣一個問題:你來說說有哪些線程安全的類?我心裡一想,這我早都背好了,稀裡譁啦說了一大堆。

他又接著問:那你再來說說什麼是線程安全?——然後我就GG了。說真的,我們整天說線程安全,但是對於什麼是線程安全我們真的了解嗎?之前的我真的是了解甚微,那麼我們今天就來聊聊這個問題。

在探討線程安全之前,我們先來聊聊什麼是進程。

什麼是進程?

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

什麼是線程?

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

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

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

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

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

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

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

以上就是,一個進程運行時產生了多個線程。

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

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

什麼是線程安全?

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

Integer count = 0;publicvoidgetCount() {count ++; System.out.println(count); }

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

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

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

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

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

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

publicvoidthreadMethod(int j){int i = 1; j = j + i;}

大家覺得這段代碼是線程安全的嗎?

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

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

兩個線程同時訪問這個方法,因為沒有共享的數據,所以他們之間的行為,並不會影響其他線程的操作和結果,所以說無狀態的對象,也是線程安全的。

添加一個狀態呢?

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

publicclassThreadDemo {int count = 0; // 記錄方法的命中次數publicvoidthreadMethod(int j){ count++ ;int i = 1; j = j + i; }}

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

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

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

如何確保線程安全?

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

1、synchronized

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

publicclassThreadDemo{int count = 0; // 記錄方法的命中次數publicsynchronizedvoidthreadMethod(int j){ count++ ;int i = 1; j = j + i; }}

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

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

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

2、Lock

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

private Lock lock = new ReentrantLock(); // ReentrantLock是Lock的子類privatevoidmethod(Thread thread){lock.lock(); // 獲取鎖對象try { System.out.println("線程名:"+thread.getName() + "獲得了鎖");// Thread.sleep(2000); }catch(Exception e){ e.printStackTrace(); } finally { System.out.println("線程名:"+thread.getName() + "釋放了鎖");lock.unlock(); // 釋放鎖對象 } }

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

寫個主方法,開啟兩個線程測試一下我們的程序是否正常:

publicstaticvoidmain(String[] args){ LockTest lockTest = new LockTest();// 線程1 Thread t1 = new Thread(new Runnable() {@Overridepublicvoidrun(){// Thread.currentThread() 返回當前線程的引用 lockTest.method(Thread.currentThread()); } }, "t1");// 線程2 Thread t2 = new Thread(new Runnable() {@Overridepublicvoidrun(){ lockTest.method(Thread.currentThread()); } }, "t2"); t1.start(); t2.start(); }

結果:

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

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

我們來看下代碼:

privatevoidmethod(Thread thread){// lock.lock(); // 獲取鎖對象if (lock.tryLock()) {try { System.out.println("線程名:"+thread.getName() + "獲得了鎖");// Thread.sleep(2000); }catch(Exception e){ e.printStackTrace(); } finally { System.out.println("線程名:"+thread.getName() + "釋放了鎖");lock.unlock(); // 釋放鎖對象 } } }

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

似乎這種方法,感覺不是很完美,如果我第一個線程,拿到鎖的時間,比第二個線程進來的時間還要長,是不是也拿不到鎖對象?

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

privatevoidmethod(Thread thread) throws InterruptedException {// lock.lock(); // 獲取鎖對象// 如果2秒內獲取不到鎖對象,那就不再等待if (lock.tryLock(2,TimeUnit.SECONDS)) {try { System.out.println("線程名:"+thread.getName() + "獲得了鎖");// 這裡睡眠3秒 Thread.sleep(3000); }catch(Exception e){ e.printStackTrace(); } finally { System.out.println("線程名:"+thread.getName() + "釋放了鎖");lock.unlock(); // 釋放鎖對象 } } }

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

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

privatevoidmethod(Thread thread) throws InterruptedException {// lock.lock(); // 獲取鎖對象// 如果5秒內獲取不到鎖對象,那就不再等待if (lock.tryLock(5,TimeUnit.SECONDS)) {try { System.out.println("線程名:"+thread.getName() + "獲得了鎖"); }catch(Exception e){ e.printStackTrace(); } finally { System.out.println("線程名:"+thread.getName() + "釋放了鎖");lock.unlock(); // 釋放鎖對象 } } }

結果:這個時候我們可以看到,線程t2等到5秒獲取到了鎖對象,執行了任務代碼。

以上就是使用Lock,來保證我們線程安全的方式。

作者:一個非科班出身的屌絲男,自學半年多,找到了一份還不錯的工作,我希望做一個專注於Java領域與思維認知的公眾號,希望可以帶領更多的初學者和入門選手,通過自己努力,得到更多的技術上的提升和思維認知上的拓展。聲明:本文為公眾號一個程式設計師的成長投稿,版權歸對方所有。

徵稿啦」

CSDN 公眾號秉持著「與千萬技術人共成長」理念,不僅以「極客頭條」、「暢言」欄目在第一時間以技術人的獨特視角描述技術人關心的行業焦點事件,更有「技術頭條」專欄,深度解讀行業內的熱門技術與場景應用,讓所有的開發者緊跟技術潮流,保持警醒的技術嗅覺,對行業趨勢、技術有更為全面的認知。

如果你有優質的文章,或是行業熱點事件、技術趨勢的真知灼見,或是深度的應用實踐、場景方案等的新見解,歡迎聯繫 CSDN 投稿,聯繫方式:微信(guorui_1118,請備註投稿+姓名+公司職位),郵箱(guorui@csdn.net)。

相關焦點

  • Linux 多線程詳解 —— 線程安全
    線程安全中涉及到的概念:臨界資源:多線程中都能訪問到的資源臨界區:每個線程內部,訪問臨界資源的代碼,就叫臨界區
  • Java創建線程安全的單例singleton
    單線程的單例如果多線程下,如果第一個線程執行檢查instance是否為null,如果為null,執行創建一個Singleton1的實例,在實例未被初始化完畢的時候,第二個線程檢查instance是否為null,這時候是null
  • 如何編寫線程安全的代碼?
    值得注意的是,關於線程安全的一切問題全部圍繞著線程私有數據與線程共享數據來處理,抓住了線程私有資源和共享資源這個主要矛盾也就抓住了解決線程安全問題的核心。接下來我們看下在各種情況下該怎樣實現線程安全,依然以C/C++代碼為例,但是這裡講解的方法適用於任何語言,請放心,這些代碼足夠簡單。
  • Java 線程安全問題的本質
    getInstance() {        if (singleton == null) {            singleton = new Singleton();        }        return singleton;    }}如上,一個簡單的單例模式,按照對象的構造過程,實例化一個對象1、可以分為三個步驟
  • 線程安全代碼到底是怎麼編寫的?
    那麼我們該怎樣寫出線程安全的代碼呢?要回答這個問題,我們需要知道我們的代碼什麼時候呆在自己家裡使用私有資源,什麼時候去公共場所浪使用公共資源,也就是說你需要識別線程的私有資源和共享資源都有哪些,這是解決線程安全問題的核心所在。
  • ConcurrentHashMap實現線程安全的底層原理
    項目中經常會有多個線程要訪問同一個數據,此時比較常用的辦法是用synchronize加鎖,CAS去進行安全的累加,去實現多線程場景下的安全的更新一個數據的效果,HashMap是用的比較多,可能就是多個線程同時讀寫一個HashMap,HashMap是線程不安全的,如果對整個map去synchronized
  • 線程和進程的關係與區別
    ; (4)一個線程可以控制和操作同一進程裡的其他線程;但是進程只能操作子進程; (5)改變注線程(如優先權),可能會影響其他線程;改變父進程,不影響子進程。總結:多線程執行效率高;  多進程耗資源,安全。 7 進程的優缺點 7.1 進程的優點 1)順序程序的特點:具有封閉性和可再現性; 2)程序的並發執行和資源共享。多道程序設計出現後,實現了程序的並發執行和資源共享,提高了系統的效率和系統的資源利用率。
  • Java編寫線程安全類的7個技巧
    幾乎每個Java應用程式都會用到線程。例如,Tomcat是在單獨的工作線程中處理每個請求,胖客戶機(Fat Client)在專用工作線程中處理長時間運行的請求。本文將跟你一起探討如何以線程安全的方式來編寫類。
  • 終於把多線程和多進程徹底講明白了!
    這個鎖其實是 Python 之父想一勞永逸解決線程的安全問題(也就是禁止多線程同時運行)2. 多線程的含義說起多線程,就不得不先說什麼是線程。然而想要弄明白什麼是線程,又不得不先說什麼是進程。Python 因為 GIL 存在,同一時期肯定只有一個線程在執行,這樣這樣就是造成我們開是個線程和一個線程沒有太大區別的原因。
  • iOS多線程全套:線程生命周期,多線程的四種解決方案,線程安全問題,GCD的使用,NSOperation的使用(下)
    NSOperation實現多線程的步驟如下:1. 創建任務:先將需要執行的操作封裝到NSOperation對象中。2. 創建隊列:創建NSOperationQueue。3. 將任務加入到隊列中:將NSOperation對象添加到NSOperationQueue中。
  • 多線程有哪些優點?-Python每日3題(多線程專題)
    這裡是Python7編程挑戰-多線程專題! 每天學習3個問題,包括初級,中級,高級問題各1個。 今天是第2天!一起來呀,就7天! 參加方法:關注麥叔編程公眾號,回復「7天」入群學習和討論。
  • 為什麼SimpleDateFormat不是線程安全的?
    能說一下 SimpleDateFormat 線程安全問題嗎,以及如何解決?🙋同事小剛:用過的,平時就是在全局定義一個 static 的 SimpleDateFormat,然後在業務處理方法中直接使用的,至於線程安全... 這個... 倒是沒遇到過線程安全問題。
  • 並發的本質:線程和進程?
    本篇收錄於《offer快到碗裡來》寫在之前 "進程和線程有何區別?" 這個問題是校招面試中最最常見的問題了。很多人討厭這種背誦課本概念的問題,還請看管打住,稍後再噴;該問題還真是一個值得思考的問題。我們常常掛在嘴邊的,你有沒有經歷過什麼高並發項目,有沒有比較難以解決的高並發問題。面試時,如果說沒有遇到高並發問題似乎低人一等。
  • 面試懵了:StringBuilder為什麼線程不安全
    我:StringBuilder不是線程安全的,StringBuffer是線程安全的面試官:那StringBuilder不安全的點在哪兒?我:。。。(啞巴了)在這之前我只記住了StringBuilder不是線程安全的,StringBuffer是線程安全的這個結論,至於StringBuilder為什麼不安全從來沒有去想過。
  • 線程、進程、多線程、多進程和多任務有啥關係?
    並發處理(concurrency Processing):指一個時間段中有幾個程序都處於已啟動運行到運行完畢之間,且這幾個程序都是在同一個處理機(CPU)上運行,但任一個時刻點上只有一個程序在處理機(CPU)上運行並發的關鍵是你有處理多個任務的能力,不一定要同時。並行的關鍵是你有同時處理多個任務的能力。所以說,並行是並發的子集。線程是程序中一個單一的順序控制流程。
  • 40個Java多線程問題總結
    5、CyclicBarrier和CountDownLatch的區別兩個看上去有點像的類,都在java.util.concurrent下,都可以用來表示代碼運行到某個點上,二者的區別在於:(1)CyclicBarrier的某個線程運行到某個點上之後,該線程即停止運行,直到所有的線程都到達了這個點,所有線程才重新運行;CountDownLatch則不是,某線程運行到某個點上之後
  • 高級分享:Java多線程你真的理解透徹了嗎?帶你玩轉一次多線程!
    線程和進程這是個老生常談的問題了,作為後端程式設計師一定要分清楚線程和進程的區別,幾乎所有的作業系統都有「進程」這一概念!一任務,一程序,每一個運行中的程序就是一個進程!當程序運行時,其內部包含了多個順序執行流,每一個順序執行流就是一個線程!
  • 面試題:StringBuilder為什麼線程不安全?
    我:StringBuilder不是線程安全的,StringBuffer是線程安全的面試官:那StringBuilder不安全的點在哪兒?我:。。。(啞巴了)在這之前我只記住了StringBuilder不是線程安全的,StringBuffer是線程安全的這個結論,至於StringBuilder為什麼不安全從來沒有去想過。
  • Java中的線程(狀態轉換和線程間通信)
    Java中的線程(狀態轉換和線程間通信)什麼是線程線程是作業系統調度的最小單元,在一個進程中可以創建多個線程,進程中的線程可以共享資源,但是每個線程都有自己的線程棧空間。Java運行是從main方法開始執行,會生成一個名為main線程。Java中的線程Java中的線程Thread類,是用來創建和啟動線程。使用方法Thread.start()來啟動一個線程。實現一個線程通常有這麼幾個方法。1、繼承Thread類,重寫run方法。執行start方法啟動線程。
  • Linux 多線程詳解 —— 什麼是線程
    線程是怎樣描述的?線程實際上也是一個task_struct,工作線程拷貝主線程的task_struct,然後共用主線程的mm_struct。線程ID是在用task_struct中pid描述的,而task_struct中tgid是線程組ID,表示線程屬於該線程組,對於主線程而言,其pid和tgid是相同的,我們一般看到的進程ID就是tgid。