「鎖」是較爲經常使用的同步方法之一。在高併發環境下,激勵的鎖競爭會致使程序的性能降低。因此咱們們將在這裏討論一些有關於鎖問題以及一些注意事項。好比:前端
避免死鎖java
減小鎖粒度node
鎖分離程序員
在多核時代,使用多線程能夠明顯地提升系統的性能。但事實上,使用多線程的方式會額外增長系統的開銷。算法
對於單任務或者單線程的應用而言,其主要資源消耗都有花在任務自己。它既不須要號維護並行數據結構間的一致性狀態,也不須要爲線程的切換和調度花費時間。但對於多線程應用來講,系統除了處理功能需求外,還須要額外維護多線程環境的特有信息,如線程自己的元數據、線程的調度、線程上下文的切換等。安全
事實上,在單核CPU上,採用並行算法的效率通常要低於原始的串行算法的,其根本緣由也在於此。所以,並行計算之因此能提升系統的性能,並非由於它「少幹活」了,而是由於並行計算能夠更合理的進行任務調度,充分利用各個CPU資源。所以,合理的併發,才能將多核CPU的性能發揮到極致。性能優化
在鎖的競爭中,單個線程對鎖的持有時間與系統性能有直接的關係.應該儘量的減小鎖的佔有時間,以減小線程之間互斥的可能.減小鎖的持有時間有助於下降鎖衝突的可能性,進而提升系統的併發能力.數據結構
同步整個方法,若是在併發量較大時,使用這種對整個方法作同步的方案.會致使等待線程大量增長.多線程
public synchronized void method(){ otherMethod(); needSyncMethod(); otherMethod(); }
優化方法之一是,只在必要時進行同步,這樣就能明顯的減小線程持有鎖的時間,提升系統吞吐量.併發
public void method(){ otherMethod(); synchronized(this){ needSyncMethod(); } otherMethod(); }
減少鎖的粒度也是一種削弱多線程鎖競爭的有效手段.這種技術典型的應用場景就是ConcurrentHashMap類的實現。
對於HashMap來講,最重要的兩個方法是put()和get()。一種最天然的想法就是對整個HashMap加鎖,必然能夠獲得一個線程安全的對象。但這樣作,就會致使加鎖顆粒度太大。對於ConcurrentHashMap,它的內部進一步分了若干個小的HashMap,稱之爲(SEGMENT)。
默認狀況下,一個ConcurrentHashMap進一步細分爲16個段.若是增長表項,並非將整個HashMap加鎖,而是首先根據hashcode獲得該表項應該被放在哪一個段中,而後對該段加鎖,完成put()操做.只要被加入的數據不存放在同一個表項,則多個線程的put()操做能夠作到真正的並行.
因爲默認16個段,因此ConcurrentHashMap最多能夠同時接受16個線程同時插入(若是都不插入到不一樣的段中),從而大大提供其吞吐量。
所謂減小鎖粒度,就是指縮小鎖定對象範圍,從而減小鎖衝突的可能性,進而提升系統的併發能力.
使用讀寫鎖ReadWriteLock能夠提升系統性能.若是說減小鎖粒度是經過分割數據結構實現的,那麼讀寫鎖則是對系統功能點的分割。在讀多寫少的場合使用讀寫鎖能夠有效替身系統的併發能力。 由於讀操做自己是不會影響數據的完整性和一致性。因此講道理應該能夠容許多線同時讀。
將讀寫鎖思想進一步延伸就是鎖分離.讀寫鎖依據讀寫操做功能上的不一樣,進行了有效的鎖分離。依據應用程序的功能特色,使用相似的分離思想,也能夠對獨佔鎖進行分離.一個典型的案例就是LinkedBlockingQueue的實現。take()和put()方法雖然都對隊列進行了修改操做,但因爲是鏈表,所以,兩個操做分別做用於隊列的前端和末尾,理論上二者並不衝突。使用獨佔鎖,則要求在進行take和put操做時獲取當前隊列的獨佔鎖,那麼take和put酒不可能真正的併發,他們會彼此等待對方釋放鎖。在JDK的實現中,取而代之的是兩把不一樣的鎖,分離了take和put操做.削弱了競爭的可能性.實現類取數據和寫數據的分離,實現了真正意義上成爲併發操做。
public class LinkedBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable { //take和put之間不存在鎖競爭關係,只須要take和take之間,put和put之間進行競爭. // Lock held by take, poll, etc private final ReentrantLock takeLock = new ReentrantLock(); // Wait queue for waiting takes private final Condition notEmpty = takeLock.newCondition(); // Lock held by put, offer, etc private final ReentrantLock putLock = new ReentrantLock(); // Wait queue for waiting puts private final Condition notFull = putLock.newCondition(); public void put(E e) throws InterruptedException { if (e == null) throw new NullPointerException(); // Note: convention in all put/take/etc is to preset local var // holding count negative to indicate failure unless set. int c = -1; Node<E> node = new Node<E>(e); final ReentrantLock putLock = this.putLock; final AtomicInteger count = this.count; putLock.lockInterruptibly(); //上鎖,不能有兩個線程同時寫數據 try { while (count.get() == capacity) { //當隊列滿時,等待拿走數據後喚醒. notFull.await(); } enqueue(node); c = count.getAndIncrement(); //更新總數,count是加(getAndIncrement先獲取當前值,再給當前值加1,返回舊值) if (c + 1 < capacity) //若是舊值+1 小於 隊列長度 notFull.signal(); //喚醒等待的寫入線程.繼續寫入. } finally { putLock.unlock(); //釋放鎖 } if (c == 0) //take操做拿完數據後就一直在notEmpty等待,這個時候的count爲0,而當put操做後,成功後就能夠喚醒take操做繼續執行了.而當隊列中count不少時,這一步是不須要執行的. signalNotEmpty(); //喚醒在notEmpty等待的線程. } public E take() throws InterruptedException { E x; int c = -1; final AtomicInteger count = this.count; final ReentrantLock takeLock = this.takeLock; takeLock.lockInterruptibly(); //上鎖 try { while (count.get() == 0) { //若是隊列爲0,等待 notEmpty.await(); } x = dequeue(); c = count.getAndDecrement(); //先取原值,再減1 if (c > 1) //若是隊列大於1,本身繼續執行. notEmpty.signal(); } finally { takeLock.unlock(); } if (c == capacity) //當長度等於設定的隊列長度,就喚醒take操做. signalNotFull(); return x; } private void signalNotEmpty() { final ReentrantLock takeLock = this.takeLock; takeLock.lock(); try { notEmpty.signal(); } finally { takeLock.unlock(); } } private void signalNotFull() { final ReentrantLock putLock = this.putLock; putLock.lock(); try { notFull.signal(); } finally { putLock.unlock(); } } }
若是對一個鎖不停地進行請求,同步和釋放,其自己也會消耗系統寶貴的資源,反而不利於性能優化.虛擬機在遇到須要一連串對同一把鎖不斷進行請求和釋放操做的狀況時,便會把全部的鎖操做整合成對鎖的一次請求,從而減小對鎖的請求同步次數,這就是鎖的粗化.
public void demoMethod(){ synchronized(lock){ //doSth... } //其餘不須要同步但很快完成的事情 ..... synchronized(lock){ //doSth... } }
整合以下:
public void demoMethod(){ synchronized(lock){ //doSth... //其餘不須要同步但很快完成的事情 ..... } }
在開發過程當中,你們也應該有意識地在合理的場合進行鎖的粗化,尤爲當在循環內請求鎖時。如下是一個循環內的請求鎖的例子,在這種狀況下,意味着每次循環都有申請鎖和釋放鎖的操做。但在這種狀況下,顯然是沒有必要的。
for(int i=0;i<CIRCLE;i++){ synchronized(lock){ } }
因此,一種更合理的作法應該是在外層只請求一次鎖:
synchronized (lock){ for(int i=0;i<CIRCLE;i++){ } }
性能優化就是根據運行時的真實狀況對各個資源點進行權衡折中的過程.鎖粗化的思想和減小鎖持有時間是相反的,但在不一樣場合,他們的效果並不相同.因此你們要根據實際狀況進行權衡.
偏向鎖是一種針對加鎖操做的優化手段,他的核心思想是:若是一個線程得到了鎖,那麼鎖就進行偏向模式.當這個線程再次請求鎖時,無需再作任何同步操做.這樣就節省了大量操做鎖的動做,從而提升程序性能.
所以,對於幾乎沒有鎖競爭的場合,偏向鎖有比較好的優化效果.由於極有可能連續屢次是同一個線程請求相同的鎖.而對於鎖競爭激烈的程序,其效果不佳.
使用Java虛擬機參數:-XX:+UseBiasedLocking 能夠開啓偏向鎖.
若是偏向鎖失敗,虛擬機並不會當即掛起線程.它還會使用一種稱爲輕量級的鎖的優化手段.輕量級鎖只是簡單的將對象頭部做爲指針,指向持有鎖的線程堆棧內部,來判斷一個線程是否持有對象鎖.若是線程得到輕量鎖成功,則能夠順利進入臨界區.若是失敗,則表示其餘線程爭搶到了鎖,那麼當前線程的鎖請求就會膨脹爲重量級鎖.
鎖膨脹後,虛擬機爲了不線程真實的在操做系統層面掛起,虛擬機還作了最後的努力就是自旋鎖.若是一個線程暫時沒法得到索,有可能在幾個CPU時鐘週期後就能夠獲得鎖,
那麼簡單粗暴的掛起線程多是得不償失的操做.虛擬機會假設在很短期內線程是能夠得到鎖的,因此會讓線程本身空循環(這即是自旋的含義),若是嘗試若干次後,能夠獲得鎖,那麼久能夠順利進入臨界區,
若是還得不到,纔會真實地講線程在操做系統層面掛起.
鎖消除是一種更完全的鎖優化,Java虛擬機在JIT編譯時,經過對運用上下文的掃描,去除不可能存在的共享資源競爭鎖,節省毫無心義的資源開銷.
咱們可能會問:若是不可能存在競爭,爲何程序員還要加上鎖呢?
在Java軟件開發過程當中,咱們必然會用上一些JDK的內置API,好比StringBuffer、Vector等。你在使用這些類的時候,也許根本不會考慮這些對象到底內部是如何實現的。好比,你頗有可能在一個不可能存在併發競爭的場合使用Vector。而周所衆知,Vector內部使用了synchronized請求鎖,以下代碼:
public String [] createString(){ Vector<String> v = new Vector<String>(); for (int i =0;i<100;i++){ v.add(Integer.toString(i)); } return v.toArray(new String[]{}); }
上述代碼中的Vector,因爲變量v只在createString()函數中使用,所以,它只是一個單純的局部變量。局部變量是在線程棧上分配的,屬於線程私有的數據,所以不可能被其餘線程訪問。因此,在這種狀況下,Vector內部全部加鎖同步都是沒有必要的。若是虛擬機檢測到這種狀況,就會將這些無用的鎖操做去除。
鎖消除設計的一項關鍵技術是逃逸分析,就是觀察某個變量是否會跳出某個做用域(好比對Vector的一些操做).在本例中,變量v顯然沒有逃出createString()函數以外。以次爲基礎,虛擬機才能夠大膽將v內部逃逸出當前函數,也就是說v有可能被其餘線程訪問。若是是這樣,虛擬機就不能消除v中的鎖操做。
逃逸分析必須在-server模式下進行,可使用-XX:+DoEscapeAnalysis參數打開逃逸分析。使用-XX:+EliminateLocks參數能夠打開鎖消除。