線程安全知多少

線程是把雙刃劍:多線程會導致性能問題(線程引入的開銷和上下文切換)

不管業務中遇到怎樣的多線程的訪問某個對象或者某個方法的情況,而在編程這個業務邏輯的時候,都不需要額外做任何的處理(也就是可以像單線程編程一樣),程序也可以正常的運行(不會因爲多線程而出錯),就可以稱爲線程安全, 相反,如果在編程的時候,需要考慮這些線程在運行時的調度和交替(例如在get調用到的期間不能調用set),或者需要額外的同步(比如使用synchronized關鍵字等),那麼就是線程安全的

線程不安全分類:

1.運行結果錯誤:a++多線程下出現消失的請求現象
2.活躍性問題:死鎖、活鎖、飢餓
3.對象發佈和初始化的時候的安全問題
什麼是發佈:對象可以超出本類的地方使用,或者作爲一個參數或retrue
1):返回一個private對象(private本意是不讓外訪問)
返回副本
2):還爲完成初始化(構造函數沒完全執行完畢)就把對象提供給外界,比如:
a.在構造函數中未初始化完就this賦值
b.隱式溢出—註冊監聽事件
c.構造函數中運行線程

那些場景需要額外注意線程安全問題?

1.訪問共享變量或資源,會由併發危險,比如對象的屬性、靜態變量、共享緩存、數據庫等
2.所有依賴時序的操作:即使每一步都是線程安全的,還是會存在併發問題:
一個線程讀取了一個共享數據,並在此基礎上做了修改例如:index++
3.不同的數據之間存在綁定關係的時候
多個線程對多個共享數據進行更新:如果這些共享數據之間存在關聯關係,那麼爲了保障操作的原子性我們可以考慮使用鎖。例如關於服務器的配置信息可能包括主機ip地址、端口號等。一個線程如果要對這些數據進行更新,則必須要保證更新操作的原子性,即主機ip地址和端口號是一起被更新的,否則其他線程可能看到一個並不真實存在的主機ip地址和端口號組合所代表的服務器
4.我們使用其他類的時候,如果對方沒有申明自己是線程安全的,那麼大概率存在在併發問題的隱患

多線程會導致的性能問題:

性能問題有哪些體現、什麼是性能問題:
處理速度慢,資源消耗大、吞吐量降低等。
爲什麼多線程會帶來性能問題:
體現在兩個方面:線程的調度和協作,這兩方面通常相輔相成,也就是說,由於線程需要協作,所以會引起調度:

調度:上下文切換

什麼時候會需要線程調度呢?當可運行的線程超過cpu核心數,那麼操作系統就要調度線程,以便於讓每個線程都有運行的機會。

調度會引起上下文切換。

例如當某個運行Thread.sleep(1000)的時候,線程調度器就會讓當前這個線程阻塞,然後往往會讓另一個正在等待的CPU資源的線程進入可運行狀態,這裏會產生上下文切換,這是一種比較大的開銷,有時上下文切換到的開銷甚至比線程執行的時間都要長。

什麼是上下文?
上下文是指某一時刻cpu寄存器和程序計數器的內容。寄存器佔CPU內部的數量較少但是速度很快的內存。寄存器通過對常用值(通常是運算的中間值)的快速訪問來提高計算器程序的運算速度。程序計數器是一個專用的寄存器,由於表明指令序列中CPU正在執行的位置,存的值未正在執行的指令的位置或則下一個將要執行的指令的位置,具體依賴於特定的操作系統。
上下文切換可以認爲是內核(操作系統的核心數)在cpu上對於進程(包括線程)進行一下活動:(1)掛機去一個線程,將這個線程在cpu的狀態(上下文)存儲於內存中某處,(2)在內存中檢索下一個進程的上下文並將其在cpu的寄存器恢復,(3)跳轉到程序計數所指向的位置(即跳轉到進程被中斷時的位置),以恢復該進程。

緩存開銷
除了剛纔提到了上下文切換帶來的直接開銷外,還需要考慮到間接帶來的緩存失效的問題。我們知道程序會會有很大的概率會訪問剛纔訪問過的數據。所以cpu爲了加快執行速度,會根據不同的算法,把常用到的數據緩存到cpu內,這樣以後再用到該數據時,可以很快使用。
但是現在上下文切換了,也就是說,cpu即將執行不同的代碼,那麼原本緩存的內容有極大的概率也沒有價值了。這就需要cpu重新緩存,者導致線程再被調度運行後,一開始啓動速度會變慢。

何時會導致密集的上下文切換
如果程序頻繁的競爭鎖,或者由於IO讀寫等原因導致的頻繁阻塞,那麼這個程序就可能需要跟多的上下文切換,這也導致了更大的開銷

協作:內存同步

線程之間如果使用共享數據,那麼爲了避免數據混亂,肯定使用同步手段,爲了數據的正確性,同步手段往往會使用禁止編譯器優化、、使CPU內的緩存失效等手段,這顯然帶來額外的開銷,因爲減少了原本可以進行的優化。

Java內存模型------底層原理:

三兄弟:JVM內存結構VS java內存模型VS java對象模型
容易混淆:三個截然不同的概念,但是很多人容易混淆
**JVM內存結構,**和java虛擬機的運行時區域有關
在這裏插入圖片描述
堆:佔用內存最多,存放引用數據類型,對象實例和數組
虛擬機棧:基本數據類型和對象的引用,編譯的時候確定了大小而且不會被改變
方法區:靜態變量,類信息,常量信息,永久引用(static)
本地方法棧:本地方法相關
程序計數器:佔最小的內存,保存字節碼的行號數

Java內存模型,和java的併發編程有關

Java對象模型,和java對象在虛擬機中的表現形式有關,需要一個標準,讓多線程運行結果可預期
Volatile、synchronized、lock等的底層原理都是JMM

重排序:
實際執行順序和代碼在java文件中的順序不一致
在這裏插入圖片描述
優點:提高處理速度,減少指令

可見性:

在這裏插入圖片描述
Cpu有多級緩存,導致讀的數據過期:1.高速緩存的容量比內存小,但是速度僅次於寄存器,所以在cpu和主內存就多了Cache層
2.線程間的對於共享變量的可見性問題不是直接由多核引起的,而是由多緩存引起的
3.如個多個核心都只用一個緩存,那麼也就不存在可見性問題了
4.每個核心都會將自己需要的數據讀到獨佔緩存中,數據修改後也要寫入緩存,然後等待刷入到主存中。所以會導致有些核心讀取的是一個過期值
在這裏插入圖片描述
什麼是主內存和工作內存

1.所有變量都存在主內存中,同時每個線程也有自己獨立的工作內存,工作內存中的變量內容是主內存的拷貝
2.線程不能直接讀寫主內存中的變量,而是隻能操作自己工作內存的變量,然後同步到主內存中
3.主內存是多個線程共享的,但是線程間不共享工作內存,如果線程間需要通信,必須藉助主內存中轉完成,所有的共享變量存在於主內存中,而且線程讀寫共享數據也是通過本地內存交換的,所以纔會導致可見性問題,

解決可見性:Happens-before::在時間上,動作a發生在動作b之前,b保證能看見a,這就是happens-before

Happens-before規則有哪些?
1.單線程原則:
單線程中的每一個操作都happens-before該程序順序中稍後出現的該線程中的每一個操作。如果操作x和操作y是同一個線程兩個操作,並且在代碼執行上x先於y出現,那麼hb(x,y)在這裏插入圖片描述
並不是受x操作一定要在y操作之前被執行,而是說x的執行結果對於y是可見的。只要滿足可見性
2.鎖操作(Lock/synchronized)
在這裏插入圖片描述
3**.Volatile變量**

4.線程啓動

5**.線程join**
我們知道join可以讓線程之間等待,假設線程A通過調用thread.start()生成一個新的線程B,然後在調用thread.join()。線程A在Join期間會等待,知道線程B的run方法執行完成,在join方法返回後,線程A中的所有後續操作將看到線程Brun方法執行的操作

在這裏插入圖片描述

6.傳遞性

7.中斷
一個線程被其他線程interrupt時,那麼檢查中斷(isInterrupted)或者拋出InterrupedException一定能看到

8.構造方法
對象構造方法的最後一行指令happens-before於finalize()方法的第一行指令

9.工具類的happens-before:
a.線程安全的容器get一定能看到在此之前的put等存入動作
b.CountDownLatch
c.Semapjore
d.Future
e.線程池
f. Cyclicbarrier

原子性:
Volatile:

volatile是一種同步機制,比synchronized或者Lock相關類更加輕量,因爲使用volatile並不會發生上下文切換等開銷很大的行爲

如果一個變量被修飾成volatile,那麼JVM就知道了這個變量可能會被併發修改

但是開銷小,相應的能力也小,雖然說volatile是用來同步的保證線程安全的,但是volatile做不到synchronized那樣的原子保護,volatile僅在有限的場景下能發揮作用

不適用於:a++這樣的不具有原子操作

使用場合:
1.Booblean flag,如果一個共享變量自始自終只被各個線程賦值,而沒有其他操作,呢麼就可以用volatile來代替synchronized或者代替原子變量,因爲賦值自身是具有原子性的,而volatile又保證可見性,所以線程安全
2.作爲刷新之前的觸發器:用了volatile int x,可以保證讀取x後,之前的所有變量可見

volatile的作用:可見性–讀一個volatile變量,需要首先讓本地緩存失效,這樣就必須到主內存中讀取最新值,寫一個volatile屬性會立即寫入到主內存中去
禁止指令重排序優化-–解決單例雙重鎖亂序問題

Volatile和synchronized的關係:volatile可以做是synchronized的輕量版:如果一個共享變量自始自終只被各個線程賦值,而沒有其他的操作,那麼就可以用volatile來代替synchronized或者代替原子變量,因爲賦值具有原子性,而volatile又保證可見性,所以保證線程安全
小結:1) .volatile修飾符適用於一下場景:某個屬性被多個線程共享,其中有一個線程修改了額此屬性,其他線程立即能獲得修改後的值,比如boolean flag
2). volatile屬性的讀寫操作是無鎖的,他不能代替synchronized,因爲沒有提供原子性和互斥性。因爲無鎖,不需要花費時間在獲取鎖和釋放鎖上,所以成本低
3**)Volatile只能用作於屬性**,我們用volatile修飾屬性,compilers就不會對這個屬性做指令重排序
4)Volatile提供可見性,任何一個線程對其的需改將立即對其他線程可見。Volatile屬性不會被線程緩存,始終從主存中讀
5)Volatile提供了happens-before保證,對volatile變量v的寫入happens-before其他線程後續對v的操作
6)Volatile可以使得long和double的賦值具有原子性
能保證可見性的措施:除了volatile可以然變量保證可見習性外、synchronized、lock、併發集合、Thread.join()和Thread.start等都可以保證一定的可見性,具體見happens-before

昇華:synchronized可見性的正確理解:synchronized也能保證happens-before效果
特別注意的是,synchronized不僅防止了一個線程在操作某個對象時受到其他線程干擾,同時還保證了修改後,可以立即被其他線程所看見

原子性:
一系列操作,要麼全部執行成功,要麼全部不執行,不會出現執行一般的情況,是不可分割的

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章