大數據成神之路-Java高級特性加強(Synchronized關鍵字)

請戳GitHub原文: github.com/wangzhiwubi…java

大數據成神之路系列:

請戳GitHub原文: github.com/wangzhiwubi…git

Java高級特性加強-集合程序員

Java高級特性加強-多線程github

Java高級特性加強-Synchronized面試

Java高級特性加強-volatile數據庫

Java高級特性加強-併發集合框架編程

Java高級特性加強-分佈式安全

Java高級特性加強-Zookeeperbash

Java高級特性加強-JVM網絡

Java高級特性加強-NIO

公衆號

  • 全網惟一一個從0開始幫助Java開發者轉作大數據領域的公衆號~

  • 公衆號大數據技術與架構或者搜索import_bigdata關注,大數據學習路線最新更新,已經有不少小夥伴加入了~

Java高級特性加強-Synchronized

本部分網絡上有大量的資源能夠參考,在這裏作了部分整理,感謝前輩的付出,每節文章末尾有引用列表,源碼推薦看JDK1.8之後的版本,注意甄別~ ####多線程 ###集合框架 ###NIO ###Java併發容器


Synchronized關鍵字

參考文章目錄: 感謝各位大大的勞動成果~深表敬意~ blog.csdn.net/qq_34337272… blog.csdn.net/qq_34337272… www.jianshu.com/p/d53bf830f… www.jianshu.com/p/c5058b6fe…


簡介

Java併發編程這個領域中synchronized關鍵字一直都是元老級的角色,好久以前不少人都會稱它爲「重量級鎖」。可是,在JavaSE 1.6以後進行了主要包括爲了減小得到鎖和釋放鎖帶來的性能消耗而引入的偏向鎖和輕量級鎖以及其它各類優化以後變得在某些狀況下並非那麼重了。

變量安全性

「非線程安全」問題存在於「實例變量」中,若是是方法內部的私有變量,則不存在「非線程安全」問題,所得結果也就是「線程安全」的了。

若是兩個線程同時操做對象中的實例變量,則會出現「非線程安全」,解決辦法就是在方法前加上synchronized關鍵字便可。

Synchronized的使用

修飾代碼塊

/**
 * 同步線程
 */
class SyncThread implements Runnable {
   private static int count;
 
   public SyncThread() {
      count = 0;
   }
 
   public  void run() {
      synchronized(this) {
         for (int i = 0; i < 5; i++) {
            try {
               System.out.println(Thread.currentThread().getName() + ":" + (count++));
               Thread.sleep(100);
            } catch (InterruptedException e) {
               e.printStackTrace();
            }
         }
      }
   }
 
   public int getCount() {
      return count;
   }
}
SyncThread的調用:
SyncThread syncThread = new SyncThread();
Thread thread1 = new Thread(syncThread, "SyncThread1");
Thread thread2 = new Thread(syncThread, "SyncThread2");
thread1.start();
thread2.start();

結果以下:

SyncThread1:0
SyncThread1:1
SyncThread1:2
SyncThread1:3
SyncThread1:4
SyncThread2:5
SyncThread2:6
SyncThread2:7
SyncThread2:8
SyncThread2:9
複製代碼

當兩個併發線程(thread1和thread2)訪問同一個對象(syncThread)中的synchronized代碼塊時,在同一時刻只能有一個線程獲得執行,另外一個線程受阻塞,必須等待當前線程執行完這個代碼塊之後才能執行該代碼塊。Thread1和thread2是互斥的,由於在執行synchronized代碼塊時會鎖定當前的對象,只有執行完該代碼塊才能釋放該對象鎖,下一個線程才能執行並鎖定該對象。 咱們再把SyncThread的調用稍微改一下:

Thread thread1 = new Thread(new SyncThread(), "SyncThread1");
Thread thread2 = new Thread(new SyncThread(), "SyncThread2");
thread1.start();
thread2.start();
複製代碼

結果以下:

SyncThread1:0
SyncThread2:1
SyncThread1:2
SyncThread2:3
SyncThread1:4
SyncThread2:5
SyncThread2:6
SyncThread1:7
SyncThread1:8
SyncThread2:9
複製代碼

不是說一個線程執行synchronized代碼塊時其它的線程受阻塞嗎?爲何上面的例子中thread1和thread2同時在執行。這是由於synchronized只鎖定對象,每一個對象只有一個鎖(lock)與之相關聯,而上面的代碼等同於下面這段代碼:

SyncThread syncThread1 = new SyncThread();
SyncThread syncThread2 = new SyncThread();
Thread thread1 = new Thread(syncThread1, "SyncThread1");
Thread thread2 = new Thread(syncThread2, "SyncThread2");
thread1.start();
thread2.start();
複製代碼

這時建立了兩個SyncThread的對象syncThread1和syncThread2,線程thread1執行的是syncThread1對象中的synchronized代碼(run),而線程thread2執行的是syncThread2對象中的synchronized代碼(run);咱們知道synchronized鎖定的是對象,這時會有兩把鎖分別鎖定syncThread1對象和syncThread2對象,而這兩把鎖是互不干擾的,不造成互斥,因此兩個線程能夠同時執行。

修飾一個方法 Synchronized修飾一個方法很簡單,就是在方法的前面加synchronized,public synchronized void method(){//todo}; synchronized修飾方法和修飾一個代碼塊相似,只是做用範圍不同,修飾代碼塊是大括號括起來的範圍,而修飾方法範圍是整個函數。

public synchronized void run() {
   for (int i = 0; i < 5; i ++) {
      try {
         System.out.println(Thread.currentThread().getName() + ":" + (count++));
         Thread.sleep(100);
      } catch (InterruptedException e) {
         e.printStackTrace();
      }
   }
複製代碼

修飾一個靜態的方法 Synchronized也可修飾一個靜態方法,用法以下:

public synchronized static void method() {
   // todo
}
複製代碼

咱們知道靜態方法是屬於類的而不屬於對象的。一樣的,synchronized修飾的靜態方法鎖定的是這個類的全部對象.

修飾一個類 Synchronized還可做用於一個類,用法以下:

class ClassName {
   public void method() {
      synchronized(ClassName.class) {
         // todo
      }
   }
}
複製代碼

總結:

34110231aa12f351a94b5384a1245a59.png
A. 不管synchronized關鍵字加在方法上仍是對象上,若是它做用的對象是非靜態的,則它取得的鎖是對象;若是synchronized做用的對象是一個靜態方法或一個類,則它取得的鎖是對類,該類全部的對象同一把鎖。 B. 每一個對象只有一個鎖(lock)與之相關聯,誰拿到這個鎖誰就能夠運行它所控制的那段代碼。 C. 實現同步是要很大的系統開銷做爲代價的,甚至可能形成死鎖,因此儘可能避免無謂的同步控制。

Synchronized的原理

對象鎖(monitor)機制

如今咱們來看看synchronized的具體底層實現。先寫一個簡單的demo:

public class SynchronizedDemo {
    public static void main(String[] args) {
        synchronized (SynchronizedDemo.class) {
        }
        method();
    }

    private static void method() {
    }
}
複製代碼

上面的代碼中有一個同步代碼塊,鎖住的是類對象,而且還有一個同步靜態方法,鎖住的依然是該類的類對象。編譯以後,切換到SynchronizedDemo.class的同級目錄以後,而後用javap -v SynchronizedDemo.class查看字節碼文件:

98cdb1130796f19ed87ac94054035d7c.png
synchronized關鍵字基於上述兩個指令實現了鎖的獲取和釋放過程,解釋器執行monitorenter時會進入到InterpreterRuntime.cpp的InterpreterRuntime::monitorenter函數,具體實現以下:
0ffb2d827a6b326cd8ad5b40b444eb71.png
執行同步代碼塊後首先要先執行monitorenter指令,退出的時候monitorexit指令。經過分析以後能夠看出,使用Synchronized進行同步,其關鍵就是必需要對對象的監視器monitor進行獲取,當線程獲取monitor後才能繼續往下執行,不然就只能等待。而這個獲取的過程是互斥的,即同一時刻只有一個線程可以獲取到monitor。上面的demo中在執行完同步代碼塊以後緊接着再會去執行一個靜態同步方法,而這個方法鎖的對象依然就這個類對象,那麼這個正在執行的線程還須要獲取該鎖嗎?答案是沒必要的,從上圖中就能夠看出來,執行靜態同步方法的時候就只有一條monitorexit指令,並無monitorenter獲取鎖的指令。這就是鎖的重入性,即在同一鎖程中,線程不須要再次獲取同一把鎖。Synchronized先天具備重入性。每一個對象擁有一個計數器,當線程獲取該對象鎖後,計數器就會加一,釋放鎖後就會將計數器減一。

synchronized的happens-before關係
什麼是happens-before

概念 happens-before的概念最初由Leslie Lamport在其一篇影響深遠的論文(《Time,Clocks and the Ordering of Events in a Distributed System》)中提出,有興趣的能夠google一下。JSR-133使用happens-before的概念來指定兩個操做之間的執行順序。因爲這兩個操做能夠在一個線程以內,也能夠是在不一樣線程之間。 所以,JMM能夠經過happens-before關係向程序員提供跨線程的內存可見性保證(若是A線程的寫操做a與B線程的讀操做b之間存在happens-before關係,儘管a操做和b操做在不一樣的線程中執行,但JMM向程序員保證a操做將對b操做可見)。具體的定義爲: 1)若是一個操做happens-before另外一個操做,那麼第一個操做的執行結果將對第二個操做可見,並且第一個操做的執行順序排在第二個操做以前。 2)兩個操做之間存在happens-before關係,並不意味着Java平臺的具體實現必需要按照happens-before關係指定的順序來執行。若是重排序以後的執行結果,與按happens-before關係來執行的結果一致,那麼這種重排序並不非法(也就是說,JMM容許這種重排序)。 上面的1)是JMM對程序員的承諾。從程序員的角度來講,能夠這樣理解happens-before關係:若是A happens-before B,那麼Java內存模型將向程序員保證——A操做的結果將對B可見,且A的執行順序排在B以前。注意,這只是Java內存模型向程序員作出的保證! 上面的2)是JMM對編譯器和處理器重排序的約束原則。正如前面所言,JMM實際上是在遵循一個基本原則:只要不改變程序的執行結果(指的是單線程程序和正確同步的多線程程序),編譯器和處理器怎麼優化都行。JMM這麼作的緣由是:程序員對於這兩個操做是否真的被重排序並不關心,程序員關心的是程序執行時的語義不能被改變(即執行結果不能被改變)。所以,happens-before關係本質上和as-if-serial語義是一回事。

具體規則

具體規則以下:

  1. 程序順序規則:一個線程中的每一個操做,happens-before於該線程中的任意後續操做。
  2. 監視器鎖規則:對一個鎖的解鎖,happens-before於隨後對這個鎖的加鎖。
  3. volatile變量規則:對一個volatile域的寫,happens-before於任意後續對這個volatile域的讀。
  4. 傳遞性:若是A happens-before B,且B happens-before C,那麼A happens-before C。
  5. start()規則:若是線程A執行操做ThreadB.start()(啓動線程B),那麼A線程的ThreadB.start()操做happens-before於線程B中的任意操做。
  6. join()規則:若是線程A執行操做ThreadB.join()併成功返回,那麼線程B中的任意操做happens-before於線程A從ThreadB.join()操做成功返回。
  7. 程序中斷規則:對線程interrupted()方法的調用先行於被中斷線程的代碼檢測到中斷時間的發生。
  8. 對象finalize規則:一個對象的初始化完成(構造函數執行結束)先行於發生它的finalize()方法的開始。
synchronized的happens-before關係

Synchronized的happens-before規則,即監視器鎖規則:對同一個監視器的解鎖,happens-before於對該監視器的加鎖。繼續來看代碼:

public class MonitorDemo {
    private int a = 0;

    public synchronized void writer() {     // 1
        a++;                                // 2
    }                                       // 3

    public synchronized void reader() {    // 4
        int i = a;                         // 5
    }                                      // 6
}
複製代碼

該代碼的happens-before關係如圖所示:

b3d7851276b01f579cac06a858d67df7.png
在圖中每個箭頭鏈接的兩個節點就表明之間的happens-before關係,黑色的是經過程序順序規則推導出來,紅色的爲監視器鎖規則推導而出:線程A釋放鎖happens-before線程B加鎖,藍色的則是經過程序順序規則和監視器鎖規則推測出來happens-befor關係,經過傳遞性規則進一步推導的happens-before關係。如今咱們來重點關注2 happens-before 5,經過這個關係咱們能夠得出什麼? 根據happens-before的定義中的一條:若是A happens-before B,則A的執行結果對B可見,而且A的執行順序先於B。線程A先對共享變量A進行加一,由2 happens-before 5關係可知線程A的執行結果對線程B可見即線程B所讀取到的a的值爲1。

synchronized的優化

經過上面的討論如今咱們對Synchronized應該有所印象了,它最大的特徵就是在同一時刻只有一個線程可以得到對象的監視器(monitor),從而進入到同步代碼塊或者同步方法之中,即表現爲互斥性(排它性)。這種方式確定效率低下,每次只能經過一個線程,既然每次只能經過一個,這種形式不能改變的話,那麼咱們能不能讓每次經過的速度變快一點了。打個比方,去收銀臺付款,以前的方式是,你們都去排隊,而後去紙幣付款收銀員找零,有的時候付款的時候在包裏拿出錢包再去拿出錢,這個過程是比較耗時的,而後,支付寶解放了你們去錢包找錢的過程,如今只須要掃描下就能夠完成付款了,也省去了收銀員跟你找零的時間的了。一樣是須要排隊,但整個付款的時間大大縮短,是否是總體的效率變高速率變快了?這種優化方式一樣能夠引伸到鎖優化上,縮短獲取鎖的時間。

CAS操做

這裏作一個介紹,CAS爲後續鎖的章節作一個鋪墊O(∩_∩)O~

推薦文章:www.jianshu.com/p/24ffe531e… 什麼是CAS? 使用鎖時,線程獲取鎖是一種悲觀鎖策略,即假設每一次執行臨界區代碼都會產生衝突,因此當前線程獲取到鎖的時候同時也會阻塞其餘線程獲取該鎖。而CAS操做(又稱爲無鎖操做)是一種樂觀鎖策略,它假設全部線程訪問共享資源的時候不會出現衝突,既然不會出現衝突天然而然就不會阻塞其餘線程的操做。所以,線程就不會出現阻塞停頓的狀態。那麼,若是出現衝突了怎麼辦?無鎖操做是使用CAS(compare and swap)又叫作比較交換來鑑別線程是否出現衝突,出現衝突就重試當前操做直到沒有衝突爲止。

CAS的操做過程 CAS比較交換的過程能夠通俗的理解爲CAS(V,O,N),包含三個值分別爲:V 內存地址存放的實際值;O 預期的值(舊值);N 更新的新值。當V和O相同時,也就是說舊值和內存中實際的值相同代表該值沒有被其餘線程更改過,即該舊值O就是目前來講最新的值了,天然而然能夠將新值N賦值給V。反之,V和O不相同,代表該值已經被其餘線程改過了則該舊值O不是最新版本的值了,因此不能將新值N賦給V,返回V便可。當多個線程使用CAS操做一個變量是,只有一個線程會成功,併成功更新,其他會失敗。失敗的線程會從新嘗試,固然也能夠選擇掛起線程 CAS的實現須要硬件指令集的支撐,在JDK1.5後虛擬機纔可使用處理器提供的CMPXCHG指令實現。 CAS的應用場景 在J.U.C包中利用CAS實現類有不少,能夠說是支撐起整個concurrency包的實現,在Lock實現中會有CAS改變state變量,在atomic包中的實現類也幾乎都是用CAS實現,關於這些具體的實現場景在以後會詳細聊聊,如今有個印象就行了(微笑臉)。 CAS的問題

  1. ABA問題 由於CAS會檢查舊值有沒有變化,這裏存在這樣一個有意思的問題。好比一箇舊值A變爲了成B,而後再變成A,恰好在作CAS時檢查發現舊值並無變化依然爲A,可是實際上的確發生了變化。解決方案能夠沿襲數據庫中經常使用的樂觀鎖方式,添加一個版本號能夠解決。原來的變化路徑A->B->A就變成了1A->2B->3C。java這麼優秀的語言,固然在java 1.5後的atomic包中提供了AtomicStampedReference來解決ABA問題,解決思路就是這樣的。
  2. 自旋時間過長 使用CAS時非阻塞同步,也就是說不會將線程掛起,會自旋(無非就是一個死循環)進行下一次嘗試,若是這裏自旋時間過長對性能是很大的消耗。若是JVM能支持處理器提供的pause指令,那麼在效率上會有必定的提高。
  3. 只能保證一個共享變量的原子操做 當對一個共享變量執行操做時CAS能保證其原子性,若是對多個共享變量進行操做,CAS就不能保證其原子性。有一個解決方案是利用對象整合多個共享變量,即一個類中的成員變量就是這幾個共享變量。而後將這個對象作CAS操做就能夠保證其原子性。atomic中提供了AtomicReference來保證引用對象之間的原子性。

GitHub: github.com/wangzhiwubi…

關注公衆號,內推,面試,資源下載,關注更多大數據技術~
                   預計更新500+篇文章,已經更新50+篇~ 
複製代碼
相關文章
相關標籤/搜索