final域、Atomic和ThreadLocal

a_higher發表於2020-11-17

解決併發2種方式

對於併發工作,需要某種方式來防止兩個任務同時訪問相同的資源,至少在關鍵階段不能出現這種衝突情況。

  • 資源被一個任務使用時,在其上加鎖
  • 根除對變數的共享。執行緒本地儲存是一種自動化機制,可以為使用相同變數的每個不同的執行緒都建立不同的儲存。因此,如果你有5個執行緒都要使用變數x所表示的物件,那執行緒本地儲存就會生成5個用於x的不同的儲存塊。它使得你可以將狀態與執行緒關聯起來。建立和管理執行緒本地儲存可以由java.lang.ThreadLocal類來實現。

final

對於final域,編譯器和處理器要遵守兩個重排序規則

  • 在建構函式內對一個final域的寫入,與隨後把這個被構造物件的引用賦值給一 個引用變數,這兩個操作之間不能重排序。 
  • 初次讀一個包含final域的物件的引用,與隨後初次讀這個final域,這兩個操 作之間不能重排序。

寫final域重排序規則

  • JMM禁止編譯器把final域的寫重排序到建構函式之外。
  •  編譯器會在final域的寫之後,建構函式return之前,插入一個 StoreStore屏障。這個屏障禁止處理器把final域的寫重排序到構造 函式之外

讀域的重排序規則

  • 在一個執行緒中,初次讀物件引用與初次讀該物件包含的final 域,JMM禁止處理器重排序這兩個操作,編譯器會在讀final 域操作的前面插入一個LoadLoad屏障。

Happens-Before

定義

  • 是一種可見性規則,它表達的含義是前面一個操作的結 果對後續操作是可見的,是對編譯器和處理器重排序的規則

規則

程式順序規則 執行緒中的每個操作,happens- before 於該執行緒中的任意後續操作

 監視器鎖規則 對一個鎖的解鎖 Happens-Before 於後續對這個鎖的加鎖

 Volatile變數規則  對一個volatile域的寫,happens-before於任意後續對這個volatile 域的讀

傳遞性   如果A happens-before B,且B happens-before C,那麼A happensbefore C

 start()規則   如果執行緒A執行操作ThreadB.start()(啟動執行緒B),那麼A執行緒的 ThreadB.start()操作happens-before於執行緒B中的任意操作

 Join()規則  如果執行緒A執行操作ThreadB.join()併成功返回,那麼執行緒B中的任意 操作happens-before於執行緒A從ThreadB.join()操作成功返回

Atomic(無鎖工具的典範)

原子性問題解決

  • synchronized、Lock
  • J.U.C包下的Atomic類

Atomic實現原理  參考這篇

瞭解unsafe類 是java提供的獲得對物件記憶體地址訪問的類, getIntVolatile(var1, var2) 獲取執行緒間共享的變數

如AtomicInteger

  • 建構函式,把數值放進成員變數中,value宣告為volatile型別
    public AtomicInteger(int initialValue) { 
       value = initialValue;
    }
  • value的值通過內部this和valueOffset找到地址進行CAS操作

ABA問題

解決辦法:加入版本號或時間戳等標誌,如使用AtomicMarkableReference,AtomicStampedReference

由於compareAndSet只能一次改變一個值,無法同時改變newReference和newStamp,所以在實現的時候,在內部定義了一個類Pair類將newReference和newStamp變成一個物件,進行CAS操作的時候,實際上是對Pair物件的操作,Pair有2個成員變數: T reference; int stamp;

public boolean compareAndSet(V  expectedReference, V  newReference, 
                            int expectedStamp,int newStamp) {
    Pair<V> current = pair;
    return  expectedReference == current.reference && 
            expectedStamp == current.stamp &&
            ((newReference == current.reference && newStamp == current.stamp) || 
            casPair(current, Pair.of(newReference, newStamp)));
}

 Atomic包中的類按照操作的資料型別可以分成4組, 我們一般常用的AtomicInteger、AtomicReference和AtomicStampedReference

  •    AtomicBoolean,AtomicInteger,AtomicLong

         執行緒安全的基本型別的原子性操作

  •    AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray

         執行緒安全的陣列型別的原子性操作,它操作的不是整個陣列,而是陣列中的單個元素

  •    AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater

         基於反射原理物件中的基本型別(長整型、整型和引用型別)進行執行緒安全的操作

  •    AtomicReference ,AtomicMarkableReference,AtomicStampedReference

         執行緒安全的引用型別及防止ABA問題的引用型別的原子操作

 在LongAdder 與AtomicLong有區別
Atomic*(CAS+自旋)遇到的問題是,用於低併發場景。

LongAddr(CAS樂觀鎖保證原子性,通過自旋保證當次修改的最終修改成功,通過降低鎖粒度(多段鎖))加了分段鎖,當競爭不激烈的時候,所有執行緒都是通過CAS對同一個變數(Base)進行修改,當競爭激烈的時候,會將根據當前執行緒雜湊到對於Cell上進行修改(多段鎖)

ThreadLocal的使用和原理

作用

  • ThreadLocal用於儲存某個執行緒共享變數,是執行緒區域性變數,同一個 ThreadLocal 所包含的物件,在不同的 Thread 中有不同的副本。

內部組成

  • Thread類中有個變數threadLocals,這個型別為ThreadLocal中的一個內部類ThreadLocalMap,這個類沒有實現map介面,就是一個普通的Java類,但是實現的類似map的功能。ThreadLocalMap是ThreadLocal的內部類,每個資料用Entry儲存,其中的Entry繼承與WeakReference

雜湊衝突:

ThreadLocal中的hash code非常簡單,就是呼叫AtomicInteger的getAndAdd方法,引數是個固定值0x61c88647。ThreadLocalMap的結構非常簡單隻用一個陣列儲存,並沒有連結串列結構,當出現Hash衝突時採用線性查詢的方式,所謂線性查詢,就是根據初始key的hashcode值確定元素在table陣列中的位置,如果發現這個位置上已經有其他key值的元素被佔用,則利用固定的演算法尋找一定步長的下個位置,依次判斷,直至找到能夠存放的位置。如果產生多次hash衝突,處理起來就沒有HashMap的效率高,為了避免雜湊衝突,使用盡量少的threadlocal變數

注:一個ThreadLocal只能儲存一個變數的副本,如果需要多個,就得建立多個變數;我們確定使用完需要執行remove避免記憶體洩漏,使用 ThreadLocal 的時候,最好要宣告為靜態的

簡單說說get方法

主要邏輯如下:

  • 先獲取當前執行緒t;
  • 然後獲取ThreadLocalMap;
  • 如果map不為空,則以當前物件(ThreadLocal物件)為key獲取value;
  • 如果map為空,則執行初始化操作;

如果是第一次呼叫get時ThreadLocalMap如果還沒有的話是如何初始化,如下

setInitialValue的主要邏輯如下:

  • 首先通過initialValue方法生成初始值;
  • 然後獲取ThreadLocalMap;
  • 如果map不為空,則將第1步生成的值set進去,以當前物件(ThreadLocal物件)為key;
  • 如果map為空,則new一個ThreadLocalMap出來;
  • 返回生成的初始值;

ThreadLocal結構圖

相關文章