4種解決線程安全問題的方式

前言

線程安全問題,在作高併發的系統的時候,是程序員常常須要考慮的地方。怎麼有效的防止線程安全問題,保證數據的準確性?怎麼合理的最大化的利用系統資源等,這些問題都須要充分的理解並運行線程。固然關於多線程的問題在面試的時候也是出現頻率比較高的。下面就來學習一下吧!java

線程

先來看看什麼是進程和線程?程序員

進程是資源(CPU、內存等)分配的基本單位,它是程序執行時的一個實例。程序運行時系統就會建立一個進程,併爲它分配資源,而後把該進程放入進程就緒隊列,進程調度器選中它的時候就會爲它分配CPU時間,程序開始真正運行。就好比說,咱們開發的一個單體項目,運行它,就會產生一個進程。面試

線程是程序執行時的最小單位,它是進程的一個執行流,是CPU調度和分派的基本單位,一個進程能夠由不少個線程組成,線程間共享進程的全部資源,每一個線程有本身的堆棧和局部變量。線程由CPU獨立調度執行,在多CPU環境下就容許多個線程同時運行。一樣多線程也能夠實現併發操做,每一個請求分配一個線程來處理。在這裏強調一點就是:計算機中的線程和應用程序中的線程不是同一個概念。數據庫

總之一句話描述就是:進程是資源分配的最小單位,線程是程序執行的最小單位。安全

什麼是線程安全

什麼是線程安全呢?什麼樣的狀況會形成線程安全問題呢?怎麼解決線程安全呢?這些問題都是在下文中所要講述的。服務器

線程安全:當多個線程訪問一個對象時,若是不用考慮這些線程在運行時環境下的調度和交替執行,也不須要進行額外的同步,或者在調用方進行任何其餘的協調操做,調用這個對象的行爲均可以得到正確的結果,那這個對象就是線程安全的。多線程

那何時會形成線程安全問題呢?當多個線程同時去訪問一個對象時,就可能會出現線程安全問題。那麼怎麼解決呢?請往下看!併發

解決線程安全

在這裏提供4種方法來解決線程安全問題,也是最經常使用的4種方法。前提是項目在一個服務器中,若是是分佈式項目可能就會用到分佈鎖了,這個就放到後面文章來詳談了。jvm

講4種方法前,仍是先來了解一下悲觀鎖和樂觀鎖吧!分佈式

悲觀鎖,顧名思義它是悲觀的。講得通俗點就是,認爲本身在使用數據的時候,必定有別的線程來修改數據,所以在獲取數據的時候先加鎖,確保數據不會被線程修改。形象理解就是總以爲有刁民想害朕。

而樂觀鎖就比較樂觀了,認爲在使用數據時,不會有別的線程來修改數據,就不會加鎖,只是在更新數據的時候去判斷以前有沒有別的線程來更新了數據。具體用法在下面講解。

如今來看有那4種方法吧!

  • 方法一:使用synchronized關鍵字,一個表現爲原生語法層面的互斥鎖,它是一種悲觀鎖,使用它的時候咱們通常須要一個監聽對象 而且監聽對象必須是惟一的,一般就是當前類的字節碼對象。它是JVM級別的,不會形成死鎖的狀況。使用synchronized能夠拿來修飾類,靜態方法,普通方法和代碼塊。好比:Hashtable類就是使用synchronized來修飾方法的。put方法部分源碼:

    public synchronized V put(K key, V value) {
            // Make sure the value is not null
            if (value == null) {
                throw new NullPointerException();
            }

    而ConcurrentHashMap類中就是使用synchronized來鎖代碼塊的。putVal方法部分源碼:

    else {
                    V oldVal = null;
                    synchronized (f) {
                        if (tabAt(tab, i) == f) {
                            if (fh >= 0) {
                                binCount = 1;

    synchronized關鍵字底層實現主要是經過monitorenter 與monitorexit計數 ,若是計數器不爲0,說明資源被佔用,其餘線程就不能訪問了,可是可重入的除外。說到這,就來說講什麼是可重入的。這裏其實就是指的可重入鎖:指的是同一線程外層函數得到鎖以後,內層遞歸函數仍然有獲取該鎖的代碼,但不受影響,執行對象中全部同步方法不用再次得到鎖。避免了頻繁的持有釋放操做,這樣既提高了效率,又避免了死鎖。

    其實在使用synchronized時,存在一個鎖升級原理。它是指在鎖對象的對象頭裏面有一個 threadid 字段,在第一次訪問的時候 threadid 爲空,jvm 讓其持有偏向鎖,並將 threadid 設置爲其線程 id,再次進入的時候會先判斷 threadid 是否與其線程 id 一致,若是一致則能夠直接使用此對象,若是不一致,則升級偏向鎖爲輕量級鎖,經過自旋循環必定次數來獲取鎖,執行必定次數以後,若是尚未正常獲取到要使用的對象,此時就會把鎖從輕量級升級爲重量級鎖,此過程就構成了 synchronized 鎖的升級。鎖升級的目的是爲了減低了鎖帶來的性能消耗。在 Java 6 以後優化 synchronized 的實現方式,使用了偏向鎖升級爲輕量級鎖再升級到重量級鎖的方式,從而減低了鎖帶來的性能消耗。可能你又會問什麼是偏向鎖?什麼是輕量級鎖?什麼是重量級鎖?這裏就簡單描述一下吧,可以幫你更好的理解synchronized。

    偏向鎖(無鎖):大多數狀況下鎖不只不存在多線程競爭,並且老是由同一線程屢次得到。偏向鎖的目的是在某個線程得到鎖以後(線程的id會記錄在對象的Mark Word中),消除這個線程鎖重入(CAS)的開銷,看起來讓這個線程獲得了偏護。

    輕量級鎖(CAS):就是由偏向鎖升級來的,偏向鎖運行在一個線程進入同步塊的狀況下,當第二個線程加入鎖爭用的時候,偏向鎖就會升級爲輕量級鎖;輕量級鎖的意圖是在沒有多線程競爭的狀況下,經過CAS操做嘗試將MarkWord更新爲指向LockRecord的指針,減小了使用重量級鎖的系統互斥量產生的性能消耗。

    重量級鎖:虛擬機使用CAS操做嘗試將MarkWord更新爲指向LockRecord的指針,若是更新成功表示線程就擁有該對象的鎖;若是失敗,會檢查MarkWord是否指向當前線程的棧幀,若是是,表示當前線程已經擁有這個鎖;若是不是,說明這個鎖被其餘線程搶佔,此時膨脹爲重量級鎖。

  • 方法二:使用Lock接口下的實現類。Lock是juc(java.util.concurrent)包下面的一個接口。經常使用的實現類就是ReentrantLock 類,它其實也是一種悲觀鎖。一種表現爲 API 層面的互斥鎖。經過lock() 和 unlock() 方法配合使用。所以也能夠說是一種手動鎖,使用比較靈活。可是使用這個鎖時必定要注意要釋放鎖,否則就會形成死鎖。通常配合try/finally 語句塊來完成。好比:

    public class TicketThreadSafe extends Thread{
          private static int num = 5000;
          ReentrantLock lock = new ReentrantLock();
          @Override
          public void run() {
            while(num>0){
                 try {
                   lock.lock();
                   if(num>0){
                     System.out.println(Thread.currentThread().getName()+"你的票號是"+num--);
                   }
                  } catch (Exception e) {
                     e.printStackTrace();
                  }finally {
                     lock.unlock();
                  }
                }
          }
    }

    相比 synchronized,ReentrantLock 增長了一些高級功能,主要有如下 3 項:等待可中斷、可實現公平鎖,以及鎖能夠綁定多個條件。

    等待可中斷是指:當持有鎖的線程長期不釋放鎖的時候,正在等待的線程能夠選擇放棄等待,改成處理其餘事情,可中斷特性對處理執行時間很是長的同步塊頗有幫助。

    公平鎖是指:多個線程在等待同一個鎖時,必須按照申請鎖的時間順序來依次得到鎖;而非公平鎖則不保證這一點,在鎖被釋放時,任何一個等待鎖的線程都有機會得到鎖。synchronized 中的鎖是非公平的,ReentrantLock 默認狀況下也是非公平的,但能夠經過帶布爾值的構造函數要求使用公平鎖。

    public ReentrantLock(boolean fair) {
            sync = fair ? new FairSync() : new NonfairSync();
        }

    鎖綁定多個條件是指:一個 ReentrantLock 對象能夠同時綁定多個 Condition 對象,而在 synchronized 中,鎖對象的 wait() 和 notify() 或 notifyAll() 方法能夠實現一個隱含的條件,若是要和多於一個的條件關聯的時候,就不得不額外地添加一個鎖,而 ReentrantLock 則無須這樣作,只須要屢次調用 newCondition() 方法便可。

    final ConditionObject newCondition() { //ConditionObject是Condition的實現類
                return new ConditionObject();
        }
  • 方法三:使用線程本地存儲ThreadLocal。當多個線程操做同一個變量且互不干擾的場景下,可使用ThreadLocal來解決。它會在每一個線程中對該變量建立一個副本,即每一個線程內部都會有一個該變量,且在線程內部任何地方均可以使用,線程之間互不影響,這樣一來就不存在線程安全問題,也不會嚴重影響程序執行性能。在不少狀況下,ThreadLocal比直接使用synchronized同步機制解決線程安全問題更簡單,更方便,且結果程序擁有更高的併發性。經過set(T value)方法給線程的局部變量設置值;get()獲取線程局部變量中的值。當給線程綁定一個 Object 內容後,只要線程不變,就能夠隨時取出;改變線程,就沒法取出內容.。這裏提供一個用法示例:

    public class ThreadLocalTest {
          private static int a = 500;
          public static void main(String[] args) {
                new Thread(()->{
                      ThreadLocal<Integer> local = new ThreadLocal<Integer>();
                      while(true){
                            local.set(++a);   //子線程對a的操做不會影響主線程中的a
                            try {
                                  Thread.sleep(1000);
                            } catch (InterruptedException e) {
                                  e.printStackTrace();
                            }
                            System.out.println("子線程:"+local.get());
                      }
                }).start();
                a = 22;
                ThreadLocal<Integer> local = new ThreadLocal<Integer>();
                local.set(a);
                while(true){
                      try {
                            Thread.sleep(1000);
                      } catch (InterruptedException e) {
                            e.printStackTrace();
                      }
                      System.out.println("主線程:"+local.get());
                }
          }
    }

    ThreadLocal線程容器保存變量時,底層實際上是經過ThreadLocalMap來實現的。它是以當前ThreadLocal變量爲key ,要存的變量爲value。獲取的時候就是以當前ThreadLocal變量去找到對應的key,而後獲取到對應的值。源碼參考以下:

    public void set(T value) {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null)
                map.set(this, value);
            else
                createMap(t, value);
        }
         ThreadLocalMap getMap(Thread t) {
            return t.threadLocals; //ThreadLocal.ThreadLocalMap threadLocals = null;Thread類中聲明的
        }
        void createMap(Thread t, T firstValue) {
            t.threadLocals = new ThreadLocalMap(this, firstValue);
        }

    觀察源碼就會發現,其實每一個線程Thread內部有一個ThreadLocal.ThreadLocalMap類型的成員變量threadLocals,這個threadLocals就是用來存儲實際的變量副本的,鍵值爲當前ThreadLocal變量,value爲變量副本(即T類型的變量)。

    初始時,在Thread裏面,threadLocals爲空,當經過ThreadLocal變量調用get()方法或者set()方法,就會對Thread類中的threadLocals進行初始化,而且以當前ThreadLocal變量爲鍵值,以ThreadLocal要保存的副本變量爲value,存到threadLocals。

    而後在當前線程裏面,若是要使用副本變量,就能夠經過get方法在threadLocals裏面查找便可。

  • 方法四:使用樂觀鎖機制。前面已經講述了什麼是樂觀鎖。這裏就來描述哈在java開發中怎麼使用的。

    其實在表設計的時候,咱們一般就須要往表裏加一個version字段。每次查詢時,查出帶有version的數據記錄,更新數據時,判斷數據庫裏對應id的記錄的version是否和查出的version相同。若相同,則更新數據並把版本號+1;若不一樣,則說明,該數據發生了併發,被別的線程使用了,進行遞歸操做,再次執行遞歸方法,直到成功更新數據爲止。

相關文章
相關標籤/搜索