synchronized/volatile/ReentrantLock/ThreadLocal介紹

1、首先先看兩篇文章瞭解一下,篇一篇二

AQS核心思想

2、synchronized介紹

  • synchronized能保證原子性,有序性,可見性。
  • synchronized加鎖原理:使用synchronized之後,當執行同步代碼塊前首先要先執行monitorenter指令,退出的時候monitorexit指令 ,其關鍵就是必須要對對象的監視器monitor進行獲取 ,當線程獲取monitor後才能繼續往下執行,否則就只能等待。而這個獲取的過程是互斥的,即同一時刻只有一個線程能夠獲取到monitor 。
  • 每個對象擁有一個計數器,當線程獲取該對象鎖後,計數器就會加一,釋放鎖後就會將計數器減一
  • synchronized優化:優化之前的synchronized獲取鎖(對象的監視器),變現爲互斥性,也就是說線程獲取鎖是一種悲觀鎖策略 ,而優化之後採用了CAS(compare and swap比較交換) 操作(又稱爲無鎖操作)是一種樂觀鎖策略
  • CAS沒有成功它會自旋(無非就是一個死循環)進行下一次嘗試,如果這裏自旋時間過長對性能是很大的消耗
  • Jdk1.6中,鎖一共有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這幾個狀態會隨着競爭情況逐漸升級。鎖可以升級但不能降級,意味着偏向鎖升級成輕量級鎖後不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是爲了提高獲得鎖和釋放鎖的效率 。
  • synchronized可見性:當線程獲取鎖時會從主內存中獲取共享變量的最新值,釋放鎖的時候會將共享變量同步到主內存中。從而,synchronized具有可見
  • synchronized有序性:synchronized語義表示鎖在同一時刻只能由一個線程進行獲取,當鎖被佔用後,其他線程只能等待。因此,synchronized語義就要求線程在訪問讀寫共享變量時只能“串行”執行,因此synchronized具有有序性
  • synchronized原子性:因爲每次只有一個線程在執行,其他線程只能等待 ,所以只能“串行”執行 ,而volatile能保障有序性是通過內存屏障,保障可見性是通過像cpu發送一條硬件指令lock,但是對於一些複雜的業務計算volatile不能保障其原子性

3、volatile介紹

  • volatile能保證有序性,可見性,但不能保證原子性。(後面會講)
  • volatile可見性:如果對聲明瞭volatile的變量進行寫操作,JVM就會向處理器發送一條Lock前綴的指令 ,這個指令會個變量所在緩存行的數據寫回到系統主內存 ,並且這個寫回內存的操作會使得其他CPU裏緩存了該內存地址的數據無效 (是每個處理器通過嗅探在總線觀察
  • volatile保障內存有序性:我們都知道,爲了性能優化,JMM在不改變正確語義的前提下,會允許編譯器和處理器對指令序列進行重排序,那如果想阻止重排序要怎麼辦了?答案是可以添加內存屏障。
  • volatile寫是在前面和後面分別插入內存屏障,而volatile讀操作是在後面插入兩個內存屏障

4、爲什麼volatile不能保障原子性

  • 原子性:原子性是指一個操作是不可中斷的,要麼全部執行成功要麼全部執行失敗,有着“同生共死”的感覺**。及時在多個線程一起執行的時候,一個操作一旦開始,就不會被其他線程所幹擾。
  • 1、 int a = 10; 是原子操作,將10賦值給線程工作內存的變量a
    2、 a++;  讀取變量a的值,對a進行加一的操作,將計算後的值再賦值給變量a,這三個操作無法構成原子操作
     
    問題:如何讓volatile保證原子性,必須符合以下兩條規則:
    1、運算結果並不依賴於變量的當前值,或者能夠確保只有一個線程修改變量的值;
    2、變量不需要與其他的狀態變量共同參與不變約束

5、ReentrantLock介紹

  • ReentrantLock加鎖原理:ReentrantLock的實現依賴於Java同步器框架AQS。AQS使用一個整型的volatile變量(命名爲state)來維護同步狀態。
  • AQS 介紹:全稱 AbstractQueuedSynchronizer抽象隊列管理器。AQS 中有兩個重要的成員,成員變量 state。用於表示鎖現在的狀態,用 volatile 修飾,保證內存一致性。同時所用對 state 的操作都是使用 CAS 進行的。state 爲0表示沒有任何線程持有這個鎖,線程持有該鎖後將 state 加1,釋放時減1。多次持有釋放則多次加減。

  • ReentrantLock重入鎖,是實現Lock接口的一個類,也是在實際編程中使用頻率很高的一個鎖,支持重入性,表示能夠對共享資源能夠重複加鎖,即當前線程獲取該鎖再次獲取不會被阻塞 

  • ReentrantLock還支持公平鎖和非公平鎖兩種方式 ,公平鎖滿足FIFO
  • 公平鎖  VS  非公平鎖
    
    1. 公平鎖每次獲取到鎖爲同步隊列中的第一個節點,保證請求資源時間上的絕對順序,而非公平鎖有可能剛釋放鎖的線程下次繼續獲取該鎖,則有可能導致其他線程永遠無法獲取到鎖,造成“飢餓”現象。
    
    2. 公平鎖爲了保證時間上的絕對順序,需要頻繁的上下文切換,而非公平鎖會降低一定的上下文切換,降低性能開銷。因此,ReentrantLock默認選擇的是非公平鎖,則是爲了減少一部分上下文切換,**保證了系統更大的吞吐量**。

6、synchronized與ReentrantLock比較

  • 類別

    synchronized

    ReentrantLock

    存在層次

    Java的關鍵字,在jvm層面上

    是一個類 java.util.concurrent.locks

    鎖的釋放 jvm自動釋放 人工手動釋放
    鎖狀態 無法判斷 可以判斷,tryLock()方法是有返回值的

    鎖類型

    可重入 、不可判斷 、非公平鎖

    可重入 、可判斷、 非公平/公平(默認非公平)

    獲取鎖 底層通過獲取對象器,CAS改變計數器 通過AQS,AQS實際上是用volatile修飾維護了一個state

 

 

 

 

 7、atmoic原子類介紹

  • atmoic原子類是juc包下的,採用的是樂觀鎖策略去原子更新數據,在java中則是使用CAS操作具體實現。
  • CAS比較交換的過程可以通俗的理解爲CAS(V,O,N),,包含三個值分別爲:V 主內存中存放的實際值;O 線程工作內存中的值;N 準備更新的新值。當V和O相同時, 也就是說舊值和內存中實際的值相同表明該值沒有被其他線程更改過 ,就可以將N值賦值給V值,反之則不作操作。
  • 當多個線程使用CAS操作一個變量是,只有一個線程會成功,併成功更新,其餘會失敗。失敗的線程會重新嘗試,當然也可以選擇掛起線程,CAS的實現需要硬件指令集的支撐

  • 場景舉例:假設線程A和線程B同時執行getAndAddInt操作(分別跑在不同CPU上)

  1. AtomicInteger中value原始值爲3,即主內存中爲3,線程A和線程B各自持有一份value爲3的副本分別在各自的工作內存中。

  2. 線程A通過getIntVolatile(var1, var2)拿到value的值3,這時線程A被掛起

  3. 線程B也通過getIntVolatile(var1, var2)方法獲取到value值3,剛好B沒有被掛起並執行compareAdeSwapInt方法,比較內存值也爲3,成功修改內存值爲4,線程B執行完畢。

  4. 這時線程A恢復,執行compareAndSwapInt方法比較,發現自己工作內存中的value3和主內存中的4不一致,說明該值已經被其他線程搶先一步修改過了,線程A本次修改失敗,只能重新讀取重新來一遍了。

  5. 線程A重新獲取value值,因爲變量value被volatile修飾,所以其他線程對它的修改,線程A總是能夠看到,線程A繼續執行compareAndSwapInt進行比較替換,直到成功。

  6. 上述中,比較和修改是容易出錯的地方,但是CAS是Unsafe類中的一條CPU系統原語,原語的執行必須是連續的,在執行過程中不允許被中斷,即CAS是一條CPU的原子指令,所以比較和交換這一步是不會被其他線程插入的

  • CAS缺點:
  1. CAS的方式相比於鎖來說併發性加強了,但如果CAS失敗,會一直進行自旋,可能會給CPU帶來很大的開銷
  2. 只能保證一個共享變量的原子性,對多個共享變量操作可以用鎖來保證原子性。

  3. 可能會引發ABA問題,案列場景如下:

  4. 案列:如:t1,t2線程都拷貝到變量atomicInteger=1,如果B線程優先級較高或運氣好,
    第一次,t2先將atomicInteger修改爲20併成功寫入主內存,
    第二次,接着t2又拷貝到atomicInteger=20,將副本又改爲1,併成功寫回主內存。
    第三次,t1拿到主內存atomicInteger的值。可這個值已經被t2修改過兩次
    ABA解決:
    互斥同步鎖synchronized
    如果項目只在乎數值是否正確, 那麼ABA 問題不會影響程序併發的正確性。
    J.U.C 包提供了一個帶有時間戳的原子引用類 AtomicStampedReference 來解決該問題,
    它通過控制變量的版本來保證 CAS 的正確性。
  • atomic包提高原子更新基本類型的工具類,主要有這些
  1. AtomicInteger ,AtomicLong ,AtomicBoolean 這幾個類的用法基本一致,這裏以AtomicInteger爲例總結常用的方法
  2. addAndGet(int delta) :以原子方式將輸入的數值與實例中原本的值相加,並返回最後的結果;

  3. incrementAndGet() :以原子的方式將實例中的原值進行加1操作,並返回最終相加後的結果

  4. getAndSet(int newValue):將實例中的值更新爲新值,並返回舊值;

  5. getAndIncrement():以原子的方式將實例中的原值加1,返回的是自增前的舊值;

  • atomic包下提供能原子更新數組中元素的類有:

  1. AtomicIntegerArray ,AtomicLongArray ,AtomicReferenceArray ,這幾個類的用法一致,就以AtomicIntegerArray來總結下常用的方法
  2. addAndGet(int i, int delta):以原子更新的方式將數組中索引爲i的元素與輸入值相加;
  3. getAndIncrement(int i):以原子更新的方式將數組中索引爲i的元素自增加1;
  4. compareAndSet(int i, int expect, int update):將數組中索引爲i的位置的元素進行更新
  • atomic包下提供能引用類型更新的類有:
  1. AtomicReference 原子更新引用類型; ,AtomicReferenceFieldUpdater 原子更新引用類型裏的字段; AtomicMarkableReference:原子更新帶有標記位的引用類型;
  2.   private static AtomicReference<User> reference = new AtomicReference<>();
    
        public static void main(String[] args) {
            User user1 = new User("a", 1);
            reference.set(user1);
            User user2 = new User("b",2);
            User user = reference.getAndSet(user2);
            System.out.println(user);
            System.out.println(reference.get());
        }
    輸出結果:
    User{userName='a', age=1}
    User{userName='b', age=2}
    更改爲新對象的值,返回舊對象的值
    
    首先將對象User1用AtomicReference進行封裝,然後調用getAndSet方法,從結果可以看出,該方法會原子更新引用的user對象,變爲`User{userName='b', age=2}`,返回的是原來的user對象User`{userName='a', age=1}`。

8、ThreadLocal介紹

  • 簡介:在多線程編程中通常解決線程安全的問題我們會利用synchronzed或者lock控制線程對臨界區資源的同步順序從而解決線程安全的問題,但是這種加鎖的方式會讓未獲取到鎖的線程進行阻塞等待,很顯然這種方式的時間效率並不是很好。線程安全問題的核心在於多個線程會對同一個臨界區共享資源進行操作,那麼,如果每個線程都使用自己的“共享資源”,各自使用各自的,又互相不影響到彼此即讓多個線程間達到隔離的狀態,這樣就不會出現線程安全的問題。事實上,這就是一種“空間換時間”的方案,每個線程都會都擁有自己的“共享資源”無疑內存會大很多,但是由於不需要同步也就減少了線程可能存在的阻塞等待的情況從而提高的時間效率。
  • ThreadLocal這個類名可以顧名思義的進行理解,表示線程的“本地變量”,即每個線程都擁有該變量副本,達到人手一份的效果,各用各的這樣就可以避免共享資源的競爭

要想學習到ThreadLocal的實現原理,就必須瞭解它的幾個核心方法,包括怎樣存怎樣取等等,

  1. set方法設置在當前線程中threadLocal變量的值 ,該方法的源碼爲
  2. ublic void set(T value) {
    	//1. 獲取當前線程實例對象
        Thread t = Thread.currentThread();
    	//2. 通過當前線程實例獲取到ThreadLocalMap對象
        ThreadLocalMap map = getMap(t);
        if (map != null)
    		//3. 如果Map不爲null,則以當前threadLocl實例爲key,值爲value進行存入
            map.set(this, value);
        else
    		//4.map爲null,則新建ThreadLocalMap並存入value
            createMap(t, value);
    }

     

  3. get方法是獲取當前線程中threadLocal變量的值,同樣的還是來看看源碼:

  4. public T get() {
    	//1. 獲取當前線程的實例對象
        Thread t = Thread.currentThread();
    	//2. 獲取當前線程的threadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
    		//3. 獲取map中當前threadLocal實例爲key的值的entry
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
    			//4. 當前entitiy不爲null的話,就返回相應的值value
                T result = (T)e.value;
                return result;
            }
        }
    	//5. 若map爲null或者entry爲null的話通過該方法初始化,並返回該方法返回的value
        return setInitialValue();
    }

     

  • 總的來說ThreadLocal是空間換時間的做法,而加鎖操作如synchronized和ReentrantLock是時間換空間的做
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章