你未必真的了解線程安全,別騙自己,來看下怎麼實現線程安全

2020-12-04 酷扯兒

本文轉載自【微信公眾號:手機電腦雙黑客,ID:heikestudio】,經微信公眾號授權轉載,如需轉載與原文作者聯繫

什麼是進程?

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

什麼是線程?

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

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

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

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

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

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

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

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

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

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

什麼是線程安全?

當多個線程訪問某個方法時,不管你通過怎樣的調用方式、或者說這些線程如何交替地執行,我們在主程序中不需要去做任何的同步,這個類的結果行為都是我們設想的正確行為,那麼我們就可以說這個類是線程安全的。既然是線程安全問題,那麼毫無疑問,所有的隱患都是在多個線程訪問的情況下產生的,也就是我們要確保在多條線程訪問的時候,我們的程序還能按照我們預期的行為去執行,我們看一下下面的代碼。

Integer count = 0;public void getCount() {count ++; System.out.println(count); }

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

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

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

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

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

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

public void threadMethod(int j) {int i = 1; j = j + i;}1234567

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

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

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

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

添加一個狀態呢?

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

public class ThreadDemo {int count = 0; // 記錄方法的命中次數 public void threadMethod(int j) { count++ ; int i = 1; j = j + i; }}1234567891011121314

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

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

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

如何確保線程安全?

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

synchronized

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

public class ThreadDemo {int count = 0; // 記錄方法的命中次數 public synchronized void threadMethod(int j) { count++ ; int i = 1; j = j + i; }}1234567891011121314

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

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

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

lock

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

private Lock lock = new ReentrantLock(); // ReentrantLock是Lock的子類private void method(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(); // 釋放鎖對象 } }123456789101112131415

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

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

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

結果

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

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

我們來看下代碼:

private void method(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(); // 釋放鎖對象 } } }123456789101112131415

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

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

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

private void method(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(); // 釋放鎖對象 } } }12345678910111213141516171819

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

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

private void method(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(); // 釋放鎖對象 } } }12345678910111213141516

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

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

聲明:本人分享該教程是希望大家,通過這個教程了解信息安全並提高警惕!本教程僅限於教學使用,不得用於其他用途觸犯法律,本人一概不負責,請知悉!

免責聲明:本文旨在傳遞更多市場信息,不構成任何投資建議和其他非法用途。文章僅代表作者觀點,不代表手機電腦雙黑客立場。以上文章之對於正確的用途,僅適用於學習

相關焦點

  • Linux多線程編程和Linux 2.6下的NPTL
    這幾天由於工作需要,琢磨了一下Linux下的多線程的相關資料。Linux下最常用的多線程支持庫為 Pthread庫,它是glibc庫的組成部分。但是關於Pthread的說明文檔非常缺乏,特別是對POSIX多線程規範的介紹以及pthread庫中多線程實現方式的介紹實在是少之又少。而多線程編程對於系統程式設計師而言是必須掌握的技術,因此總是讓學習中的程式設計師覺得頭痛不以。
  • 線程不是你想中斷就能中斷
    為什麼不強制停止 如何用 interrupt 停止線程 sleep 期間能否感受到中斷 停止線程的方式有幾種 總結啟動線程需要調用 Thread 類的 start() 方法,並在 run() 方法中定義需要執行的任務。啟動一個線程非常簡單,但如果想要正確停止它就沒那麼容易了。
  • linux多線程之線程資源的釋放
    一般來說,對一段運行代碼進行加鎖然後解鎖,如下所示:pthread_mutex_lock(&mutex);//運行代碼;pthread_mutex_unlock(&mutex);如果在運行代碼這塊發生錯誤,有異常,導致這個線程異常退出,那麼怎麼辦,pthread_unlock
  • 進程和線程常見的19個問題
    你給別人發微信別人看後不回覆你或者幾個小時後才回覆你,你是什麼感受,這還是交互式嗎?均衡性:減少平均響應時間的波動。需要符合固有期望和預期,你給別人發微信,他有時候秒回復,有時候幾個小時後才回復。在交互式系統中,可預測性比高差異低平均更重要。
  • Java項目實踐,CountDownLatch實現多線程閉鎖
    摘要本文主要介紹Java多線程並發中閉鎖(Latch)的基本概念、原理、示例代碼、應用場景,通過學習,可以掌握多線程並發時閉鎖(Latch)的使用方法。概念「閉鎖」就是指一個被鎖住了的門將線程a擋在了門外(等待執行),只有當門打開後(其他線程執行完畢),門上的鎖才會被打開,a才能夠繼續執行。
  • 「軟帝學院」Java挑戰者專欄:多線程詳解2
    2.怎麼通信如果希望線程等待, 就調用wait()如果希望喚醒等待的線程, 就調用notify();這兩個方法必須在同步代碼中執行, 並且使用同步鎖對象來調用>多線程(線程的五種狀態)(掌握)看圖說話新建,就緒,運行,阻塞,死亡多線程(線程池的概述和使用)(了解)A:線程池概述
  • 怎麼查看佔 cpu 最多的線程
    如何定位確認是哪個線程導致的?如何確認下是哪部分代碼導致的CPU使用率偏高呢?當然CPU使用高,並不一定意味著是有問題的,下面的方法僅用於排插問題的時候使用,例子也僅作為參考。(PS下面最終查詢出來的具體代碼使用CPU高是正常情況,這個需根據具體的業務場景去確認)1. 使用top命令確認是哪個進程佔用CPU高。
  • 多線程面試和進階必備-任務同步最常用的十種鎖的原理
    線程調度InterLocked類。i++是線程不安全的,它的操作包括從內存中獲取一個值,給該值遞增1,再將它存回內存。這些操作可能被線程調度器打斷。Interlocked類提供了以線程安全的方式遞增、遞減、交換和讀取值的方式。與其他的同步技術相比,使用功能Interlocked類會快得多,但是僅用於簡單的任務同步。Monitor類。與Lock語句相比,Monitor類可以添加一個等待被鎖定的超時值。
  • Linux和Windows兩種風格的作業系統,創建線程的方式有何不同?
    Linux 管理線程的方式不同於其他一些經典作業系統,Linux 並沒有線程的概念,它把線程當作進程的一個子集來管理。因此,Linux 內核並未為線程提供額外調度算法,也沒有提供額外的數據結構用於描述和存儲線程。就像進程一樣,Linux 使用 task_struct 結構體描述和記錄線程,每個線程都有唯一屬於自己的 task_struct 結構。
  • 優雅終止線程?系統內存佔用較高?
    ETL由源端到目的端,中間的業務邏輯一般由用戶自己編寫的SQL模板實現,velocity是其中涉及的一種模板語言。Velocity之OOMVelocity的基本使用Velocity模板語言的基本使用代碼如下:1. 初始化模板引擎2.
  • CPU溫度高 微星教你一招關閉超線程功能
    如果發覺自己的CPU溫度過高了,不妨試試微星的方法——關閉沒什麼用的超線程功能。 不過超線程對遊戲來說沒什麼用,效果不明顯不說,開啟超線程也會增加功耗和發熱,為此微星給出了詳細的教程,教大家關閉超線程功能。
  • 微軟MR技術專家分享:AR/VR多線程處理的八年經驗與技巧
    日前,拜恩茲撰文分享了自己在AR/VR/MR多線程處理方面的八年經驗和技巧。下面是映維網的具體整理:  要正確實現多線程並不容易,但它對於資源受限的行動裝置流暢運行模擬至關重要。在供職於微軟的生涯中,我有機會在四年多的時間裡幫助合作夥伴為HoloLens編寫高性能的應用程式。我另外有4年多的時間幫助合作夥伴為智慧型手機和平板電腦編寫高性能應用程式。
  • Redis 2.4:後臺線程如何解決aof缺陷?
    【IT168技術】Redis終於在2.4版本裡引入了除主線程之外的後臺線程,這個事情由來已久.早在2010年2月就有人提出aof的缺陷,提及的問題主要有:  ① 主線程aof的每次fsync(everysecond模式)在高並發下時常出現100ms的延時,這源於fsync必不可少的磁碟操作,即便已經優化多次請求的離散小
  • AMD狂打雞血:12核心24線程Ryzen驚曝!
    此前曾有傳聞稱,AMD還準備了16核心32線程的發燒級桌面產品,介於8核心Ryzen 7、32核心Naples之間,同時搭配新的X399晶片組,而昨日有消息顯示,X399是面向伺服器的,桌面發燒平臺則是X390。
  • 項目實踐,使用Cyclic Barrier在多線程中設置屏障
    柵欄類似於閉鎖,它能阻塞一組線程直到某個事件的發生。柵欄與閉鎖的關鍵區別在於,所有的線程必須同時到達柵欄位置,才能繼續執行。閉鎖用於等待事件,而柵欄用於等待其他線程。Cyclic Barrer也可以用於解決多個線程之間的相互等待問題。
  • 決戰多線程!AMD速龍 631對酷睿i3 2100
    該CPU僅500元左右的售價頓時吸引了很多用戶的目光,尤其是注重多線程與多任務性能的用戶。其中不乏在Athlon II X4 631與Intel Core i3 2100中徘徊的用戶,今天,我們從多線程、多任務性能出發,考察兩款CPU的在多線程性能上的差距。
  • 6核12線程神器:Intel高頻酷睿i7-8700K評測
    八核心十六線程的R7系列處理器藉助全新的架構以及14nm的新工藝,AMD再次翻身,這次是真的翻身了,而英特爾方面由於長期沒有對手一直享受著擠牙膏的快感。令英特爾沒想到的是,AMD這次不再是人們調侃的PPT廠。為了奪回PC處理器巨頭的位置,英特爾在2017年5月「臺北國際電腦展」上發布的全新的酷睿i9處理器。
  • 十核心二十線程 酷睿i7-6950X首發評測(全文)_Intel 酷睿i7 6950X...
    藉此盛會開幕之際,Intel同步推出了旗下最新的至尊版處理器,它就是具備十核心二十線程的Intel酷睿i7-6950X。相信各位骨灰級玩家已經對這款旗艦級處理器期待已久,下面我們就來一同深入的了解一下。
  • Python入門基礎之socket多線程編程,TCP伺服器和客戶端通信
    不過那個只是單線程的,伺服器一次只能和一個客戶端會話,多個客戶端的話只能等待。我們平時的應用中,伺服器肯定是要並發的,所以,今天將介紹socket編程的多線程編程。一個伺服器同時和多個客戶端建立會話。多線程原理:TCP伺服器會創建一個線程池,每當有客戶端請求連接的時候,它便會從線程池中分配一個線程同客戶端建立連接,當客戶端中斷連接後,線程便銷毀。
  • 你項目還在用Date表示時間!
    為啥Date遭嫌棄了 別的先不說,我們先來看幾個關於 Date用法的例子,這玩意真的好用嗎? 零零散散舉了這麼些例子,我想 LocalDateTime怎麼地也不輸 Date吧! 線程安全性問題! 其實上面講來講去只講了兩者在用法上的差別,這其實倒還好,並不致命,可是接下來要討論的線程安全性問題才是致命的!