一,JAVA線程是如何實現的?
同步,涉及到多線程操作,那在JAVA中線程是如何實現的呢?
操作系統中講到,線程的實現(線程模型)主要有三種方式:
①使用內核線程實現
②使用用戶線程實現
③使用用戶線程加輕量級線程實現
二,JAVA語言定義了哪幾種線程狀態?
JAVA語言定義了五種線程狀態:①新建(New),當你 new 了一個Thread,但是並沒有調用它的 start()方法時,就處於這種狀態。
②運行(Run),這里包含了兩種狀態:一種是可運行狀態,就是你調用了Thread的start()方法之后,但是該線程還未獲得CPU(相當於操作系統中講的就緒狀態);另一種是運行狀態,就是該線程被調度器分配了CPU,正在執行。
③等待(Waiting),處於這種狀態的線程不會被分配CPU執行時間,它需要等待其他線程喚醒。等待又分成兩種:無限等待和超時等待(限期等待)。
無限等待一般是執行等待的方法沒有指定超時參數
比如Object類的wait()方法,會使線程進入無限等待狀態,它還有一個帶有 timeout(超時) 參數 的重載方法 Object.wait(long timeout),使線程超時等待。
調用Thread.sleep() 、 Object.wait()、Thread.join()方法都會使線程進入等待狀態。
④阻塞(Blocked),個人感覺阻塞是與鎖有關,而等待並不一定與鎖有關。
比如,兩個線程爭奪對象鎖(synchronized),未獲得鎖的那個線程將進入阻塞狀態。而線程進入等待狀態則有可能是因為③中提到的調用了Thread.sleep()方法,或者是線程讀某個Socket端口上的數據,但是此時數據還未到達,則線程進入等待狀態。
⑤結束(Terminated),線程的run方法運行完畢,進入結束狀態。
三,同步(加鎖)為什么會有代價?
《深入理解JVM》中講到,在目前的JDK版本中,操作系統支持怎樣的線程模型,在很大程序上決定了JAVA虛擬機的線程是怎樣映射的。JAVA的線程是映射到操作系統的原生線程之上的,如果要阻塞或喚醒一個線程,都需要操作系統幫忙完成,這就需要從用戶態切換到核心態中,因此狀態轉換需要耗費很多的處理器時間。
而在JAVA里面要實現同步,一種是使用對象鎖,即synchronized關鍵字;另一種則是使用 java.util.ReentrantLock。獲得鎖的線程進入臨界區執行、未獲得鎖的線程會阻塞,然后在某種條件下被喚醒。因此,同步(使用鎖)是有代價的。
四,對象鎖同步(synchronized同步) 與 ReentrantLock同步的區別
它們都是可重入鎖,可用來互斥同步,但主要有三個方面的區別
①使用synchronized進行同步的線程在阻塞等待時,是不可中斷的。而使用ReentrantLock進行同步的線程在阻塞等待時可中斷。
如果臨界區需要執行很長的時間,synchronized就只能一直阻塞等待了,而ReentrantLock 可以在阻塞等待一段時間之后,若還未獲得鎖,就可以被其他線程中斷,從而去干其他事情。一個很好的示例可參考:ReentrantLock鎖實現中斷線程阻塞
通過調用 lock.lockInterruptibly()方法來實現線程在阻塞等待時 可被其他線程 中斷。
②ReentrantLock可以實現公平鎖
公平鎖是指多個線程在等待同一個鎖時,必須按照申請鎖的時間順序來依次獲得鎖。而非公平鎖則按照搶占的方式來獲得鎖,synchronized中的鎖是不公平的,而ReentrantLock可以在構造函數中指定創建的鎖是否為公平鎖。
public ReentrantLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); }
③使用ReentrantLock 鎖可以同時綁定多個條件變量(Condition對象)
所謂 條件對象 就是說:線程好不容易獲得了鎖 進入臨界區,卻發現需要在滿足某一條件之后,它才能執行。因此,使用一個條件對象來管理那些已經獲得了鎖但是卻不能做有用工作的線程。
比如說:生產者--消費者模型,消費者獲得了隊列的鎖,去隊列中取產品,但是結果發現隊列為空,它沒有產品可消費。換句話說:它需要在隊列不為空的條件下,才能消費產品。(沒有產品,肯定無法消費產品啊!)
比如,消費者的代碼一般是下面這樣:進入consume()方法需要獲得鎖,但是卻發現隊列為空,故只能調用wait()方法 進入等待狀態。(不是阻塞狀態)
public synchronized void consume(){ while(queue.isEmpty()) wait();//沒有產品可消費,只能放棄鎖,並等待生產者線程生產了產品之后,喚醒它 //consume product notifyAll(); //.... }
那ReentrantLock呢?
Condition emptyCondition = lock.newCondition(); Condition fullCondition = lock.newCondition(); ....... public synchronized void consume(){ try{ lock.lock();//ReentrantLock lock while(queue.isEmpty()) emptyCondition.await(); //consume product emptyCondition.singalAll(); //.... }finally{ lock.unlock(); } }
對於同一把ReentrantLock,它可以 new多個Condition,即同一把鎖可以關聯多個條件變量。
Condition.awit()方法的JDK源碼解釋非常值得一讀。部分摘錄如下:
Causes the current thread to wait until it is signalled or
* {@linkplain Thread#interrupt interrupted}.
* <p>The lock associated with this {@code Condition} is atomically
* released and the current thread becomes disabled for thread scheduling
* purposes and lies dormant until <em>one</em> of four things happens
調用Condition.await()方法使線程放棄鎖,並進入等待狀態(不是阻塞狀態),直到有下列四種情況發生 才從等待狀態退出.....
.....
五,互斥同步 與 非阻塞同步 是什么?
所謂互斥同步,就是多個線程爭奪一把鎖時,未獲得鎖的那些線程將會阻塞。因此,互斥同步最主要的是進行線程阻塞和喚醒帶來的性能問題,因此 互斥同步稱為阻塞同步。從處理問題的方式上看,它是一種悲觀的並發策略:它認為如果不采取正確的同步措施,那執行就可能出問題。也就是說:它總是對共享數據先進行加鎖,然后再去訪問,盡管在訪問過程中 也許 並沒有 其他線程 訪問該共享數據。
而正如前面(三)中 提到,加鎖是有代價的,如果對共享數據加了鎖,但是在訪問共享數據過程中,並沒有其他線程來訪問該共享數據(即並沒有出現競爭),那這次加鎖就感覺有點浪費了。(就相當於:花了大量的人力、物力應對某次可能發生的地震,但是最終地震沒有發生)
於是,為了進一步的”優化“,就出現了基於沖突檢測的樂觀並發策略。
樂觀並發策略就是:先進行操作,如果沒有其他線程爭用共享數據,那么操作就成功了。如果數據有爭用,產生了沖突,那再采用補救措施。
因此,樂觀並發策略的很多實現都不需要把線程掛起(因為它是 先執行了 操作再說),因而稱為:非阻塞同步。
樂觀並發策略需要硬件指令集的支持。因為,它在操作的過程中需要檢測是否發生了沖突,需要“操作”和“沖突檢測”這兩個步驟具備原子性。原子性如何保證?如果用互斥同步保證 那就又變回 悲觀並發策略 了,因此只能靠硬件來保證原子性。
比如java.util.concurrent.atomic.AtomicInteger.java 中的自增加1方法getAndIncrement(),就是通過硬件原子指令:CAS指令(Compare and swap)來實現
public final int getAndIncrement() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return current; } } public final boolean compareAndSet(int expect, int update) { return unsafe.compareAndSwapInt(this, valueOffset, expect, update); }
六,關於鎖優化的一些知識
1)自旋鎖 的 實現思想
在多核CPU下,一個線程獲得鎖進入臨界區占用CPU執行時,我們有理由相信這個臨界區代碼很快就會執行完成,即: 共享數據的鎖定狀態只會持續很短的一段時間,而當另外一個線程剛好在這段時間去搶占鎖,掛起這個搶占鎖的線程有點 不值得。(畢竟占用鎖的線程很快就會把鎖釋放了呀)
於是,就讓請求搶占鎖的那個線程“稍微等待一下”,但不放棄處理器的執行時間(一旦放棄,就意味着需要掛起和恢復線程了),看持有鎖的線程是否很快就會釋放鎖。為了讓線程等待一下,我們只需讓線程執行一個忙循環(自旋),這就是:自旋鎖。
2)自旋鎖的“改進”---自適應自旋
自旋等待避免了線程切換的開銷,但是它是要占用處理器時間的。因此,如果鎖被占用的時間很短,那自旋等待的效果就會非常好;如何鎖被占用的時間很長,執行忙循環(自旋)的線程就白白消耗處理器資源,造成性能上的浪費。默認情況下,自旋的次數是10次,但可以通過JVM參數進行修改。
為了應對鎖被占用很長時間 而導致的長時間無效的自旋,自旋的時間必須有一定的限度。
那如何確定一個合適的限定呢?這就是自適應自旋的目標了。
自適應自旋對自旋的次數沒有固定,比如說:在同一個鎖對象,自旋等待剛剛成功獲得過鎖,那么這次也很有可能成功,進而允許自旋等待持續相對更長的時間。
再比如說,對於某個鎖,自旋很少成功獲得過鎖,那在以后獲取這個鎖時可省略自旋過程,以避免處理器資源浪費。
3)輕量級鎖和偏向鎖
個人感覺輕量級鎖和偏向鎖 的功能 與 緩存 的思想有點像。直接同步互斥加鎖的代價是很大的(重量級鎖),那我們可以先來一個輕量級鎖或偏向鎖。
如果在加了 輕量級鎖或偏向鎖的過程中 沒有發生其他線程來爭搶鎖(類似於緩存命中!)這意味着整個過程“幾乎”不需要同步。
如果有其他線程爭搶鎖,那輕量級鎖將不再有效(偏向鎖的偏向模式失效),輕量級鎖要膨脹為“重量級鎖”,后面等待鎖的線程要進入阻塞狀態。這就是類似於緩存未命中!
關於輕量級鎖和偏向鎖的具體解釋:可參考《深入理解JVM》
上面關於鎖的優化是JVM的一些鎖優化策略,在應用層進行鎖優化方式有如下:
①盡量減少鎖的持有時間。只對必要的需要同步的代碼進行同步。
synchronized{this} { methodA();// 把不需要同步的方法放到 sync 外面去 mutex(); //other method...// 把不需要同步的方法放到 sync 外面去 }
②減少鎖的粒度
ConcurrentHashMap就很好的應用了這種思想。它將整個HashMap分成了若干個段(Segment),每個段都有自己的鎖,每個段負責管理HashMap中的一部分HashEntry,段與段之間的HashEntry互不干擾,多線程可以並行地操作不同的Segment管理下的HashEntry。
這樣鎖的粒度就減少了。如果整個HashMap只有一把鎖管理,鎖的粒度就很大。操作HashMap不同的區域都需要互斥同步。而ConcurrentHashMap將HashMap分解成段,每個段有一把鎖,鎖的粒度就少了。但是與此同時,鎖的數量增多了。當需要訪問ConcurrentHashMap的全局屬性時(比如ConcurrentHashMap的size()方法),需要 獲得 所有的段的鎖。
1 try { 2 for (;;) { 3 if (retries++ == RETRIES_BEFORE_LOCK) { 4 for (int j = 0; j < segments.length; ++j) 5 ensureSegment(j).lock(); // force creation
以上是size()方法的部分代碼,size()的具體實現肯定也有相應的優化。
③鎖分離
鎖分離與鎖分段有點相似,鎖分離就是對不同的操作使用不同的鎖。比如,java.util.concurrent.LinkedBlockingQueue 是一個線程安全的阻塞隊列,take()方法從隊列中取元素,put()方法向隊列中添加元素。
這個隊列是用鏈式存儲結構實現,它的結點類如下:
1 /** 2 * Linked list node class 3 */ 4 static class Node<E> { 5 E item; 6 7 /** 8 * One of: 9 * - the real successor Node 10 * - this Node, meaning the successor is head.next 11 * - null, meaning there is no successor (this is the last node) 12 */ 13 Node<E> next; 14 15 Node(E x) { item = x; } 16 }
它的 put 操作 和 take 操作分別作用於隊列的尾部和頭部,並沒有相互沖突。如果 take 和 put 都共享同一把鎖,那么從隊列中取走元素的同時,就不能向隊列中添加元素,盡管它們互不“干擾”。
因此,為了提高並發效率,就使用了兩把鎖:一把 put 鎖,一把 take 鎖。這就是鎖分離機制。
1 /** Lock held by take, poll, etc */ 2 private final ReentrantLock takeLock = new ReentrantLock(); 3 4 /** Wait queue for waiting takes */ 5 private final Condition notEmpty = takeLock.newCondition(); 6 7 /** Lock held by put, offer, etc */ 8 private final ReentrantLock putLock = new ReentrantLock(); 9 10 /** Wait queue for waiting puts */ 11 private final Condition notFull = putLock.newCondition();
take鎖對應着 notEmtpy條件變量,putLock對應着一個 notFull條件變量。
在大部分情況下,多線程可以同時並行地向阻塞隊列中添加元素和取出元素。比如線程A向隊列添加元素的時候,並不阻塞線程B從隊列中取出元素。
通過使用 take 鎖 和 put 鎖,將LinkedBlockingQueue的讀寫分離。優化了並發效率。
④鎖粗化
這個與①中的盡量減少 鎖的時間 思想相反。但它們適用的情況是不同的。
比如,當程序需要在 for 循環內部加鎖時,每執行一次 for 循環中的操作就需要加鎖一次,加鎖之后執行臨界區代碼后又釋放鎖,這樣加鎖、釋放鎖的頻率非常大,效率反而低了。
1 public void syncMethod(){ 2 for(int i = 0; i < COUTN; i++) 3 { 4 synchronized(this){ 5 mutex(); 6 } 7 //do something, do not need lock 8 synchronized(this){ 9 mutex(); 10 } 11 //do another thing which needs no lock 12 } 13 }
這樣,直接用synchronized修飾方法反而要更好一點。
七,參考資料
《深入理解JVM》周志明
http://thrillerzw.iteye.com/blog/2055486
原文:http://www.cnblogs.com/hapjin/p/5765573.html