1. 程式人生 > >深入淺出Java併發包—鎖(Lock)VS同步(synchronized)

深入淺出Java併發包—鎖(Lock)VS同步(synchronized)

今天我們來探討一下Java中的鎖機制。前面我們提到,在JDK1.5之前只能通過synchronized關鍵字來實現同步,這個前面我們已經提到是屬於獨佔鎖,效能並不高,因此JDK1.5之後開始藉助JNI實現了一套高效的鎖實現!

JDK5以後鎖的介面是JUC中的Lock,我們來先看一下他的相關API文件。


方法摘要

 void

lock()獲取鎖。如果鎖不可用,出於執行緒排程目的,將禁用當前執行緒,並且在獲得鎖之前,該執行緒將一直處於休眠狀態。

 void

lockInterruptibly()

如果當前執行緒未被

中斷,則獲取鎖。

如果鎖可用,則獲取鎖,並立即返回。

如果鎖不可用,出於執行緒排程目的,將禁用當前執行緒,並且在發生以下兩種情況之一以前,該執行緒將一直處於休眠狀態:

l鎖由當前執行緒獲得;或者

l其他某個執行緒中斷當前執行緒,並且支援對鎖獲取的中斷。

如果當前執行緒:

l在進入此方法時已經設定了該執行緒的中斷狀態;或者

l在獲取鎖時被中斷,並且支援對鎖獲取的中斷,

則將丟擲 InterruptedException,並清除當前執行緒的已中斷狀態。

Condition

newCondition()返回繫結到此 Lock 例項的新 Condition 

例項。

在等待條件前,鎖必須由當前執行緒保持。呼叫 Condition.await() 將在等待前以原子方式釋放鎖,並在等待返回前重新獲取鎖。

 boolean

tryLock()僅在呼叫時鎖為空閒狀態才獲取該鎖。

如果鎖可用,則獲取鎖,並立即返回值 true。如果鎖不可用,則此方法將立即返回值 false

此方法的典型使用語句如下:

      Lock lock = ...;

      if (lock.tryLock()) {

          try {

              // manipulate protected state

          } finally {

              lock.unlock();

          }

      } else {

          // perform alternative actions

      }

此用法可確保如果獲取了鎖,則會釋放鎖,如果未獲取鎖,則不會試圖將其釋放。

 boolean

tryLock(long time, TimeUnit unit)如果鎖在給定的等待時間內空閒,並且當前執行緒未被中斷,則獲取鎖。

如果鎖可用,則此方法將立即返回值 true。如果鎖不可用,出於執行緒排程目的,將禁用當前執行緒,並且在發生以下三種情況之一前,該執行緒將一直處於休眠狀態:

l鎖由當前執行緒獲得;或者

l其他某個執行緒中斷當前執行緒,並且支援對鎖獲取的中斷;或者

l已超過指定的等待時間

如果獲得了鎖,則返回值 true

如果當前執行緒:

l在進入此方法時已經設定了該執行緒的中斷狀態;或者

l在獲取鎖時被中斷,並且支援對鎖獲取的中斷,

則將丟擲 InterruptedException,並會清除當前執行緒的已中斷狀態。

如果超過了指定的等待時間,則將返回值 false。如果 time 小於等於 0,該方法將完全不等待。

void

unlock()釋放鎖。

相對於API來說,我們並不能看出他到底的優點在哪裡?我們來看一個例項

package com.yhj.lock;
/**
 * @Described:原子int型別操作測試用例
 * @author YHJ create at 2013-4-26 下午05:58:32
 * @ClassNmae com.yhj.lock.AtomicIntegerTestCase
 */
public interface AtomicIntegerTestCase {
 
    /**
     * ++並返回
     * @return
     * @Author YHJ create at 2013-4-26 下午05:39:47
     */
    int incrementAndGet();
    /**
     * 取值
     * @return
     * @Author YHJ create at 2013-4-26 下午05:39:56
     */
    int get();
}

package com.yhj.lock;
 
 
/**
 * @Described:帶同步的測試
 * @author YHJ create at 2013-4-26 下午05:35:35
 * @ClassNmae com.yhj.lock.AtomicIntegerWithLock
 */
public class AtomicIntegerWithSynchronized implements AtomicIntegerTestCase {
   
    private int value;
    private Object lock  = new Object();//為保證兩個方法都在同步內 採用物件同步方法
   
    @Override
    public int incrementAndGet(){
       synchronized(lock){
           return ++value;
       }
    }
   
    @Override
    public int get(){
       synchronized(lock){
           return value;
       }
    }
 
}

package com.yhj.lock;
 
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
/**
 * @Described:帶鎖的測試
 * @author YHJ create at 2013-4-26 下午05:35:35
 * @ClassNmae com.yhj.lock.AtomicIntegerWithLock
 */
public class AtomicIntegerWithLock implements AtomicIntegerTestCase{
   
    private int value;
    private Lock lock  = new ReentrantLock();
   
    @Override
    public int incrementAndGet(){
       try {
           lock.lock();
           return ++value;
       }finally{
           lock.unlock();
       }
      
    }
 
    @Override
    public int get(){
       try {
           lock.lock();
           return value;
       }finally{
           lock.unlock();
       }
    }
 
}

package com.yhj.lock;
 
public class Client {
 
    /**
     * 啟動並等待執行緒結束
     * @param threads
     * @throws InterruptedException
     * @Author YHJ create at 2013-4-26 下午05:57:16
     */
    private static void startAndWait(Thread [] threads) throws InterruptedException{
       for(Thread thread:threads)
           thread.start();
       for(Thread thread:threads)
           thread.join();
    }
    /**
     * 準備執行緒資料
     * @param threads
     * @param testCase
     * @param threadCount
     * @param loopCount
     * @Author YHJ create at 2013-4-26 下午06:01:25
     */
    private static void prepare(Thread [] threads,final AtomicIntegerTestCase testCase,int threadCount,final int loopCount){
       for (int i = 0; i < threadCount; i++) {
           threads[i] = new Thread(){
              @Override
              public void run() {
                  for(int i=0;i<loopCount;++i){
                     testCase.incrementAndGet();
                  }
              }
           };
       }
    }
 
    public static void main(String[] args) throws InterruptedException {
       //前期資料準備
       final int threadCount = 200;//執行緒數目
       final int loopCount = 100000;//迴圈次數
       final AtomicIntegerWithLock lockTestCase = new AtomicIntegerWithLock();//測試物件
       final AtomicIntegerWithSynchronized synchronizedtestCase = new AtomicIntegerWithSynchronized();
       //第一波資料準備
       Thread [] threads= new Thread[threadCount];
       prepare(threads, lockTestCase, threadCount, loopCount);
       //第一波啟動
       long costTime = 0;
       long start = System.nanoTime();
       startAndWait(threads);
       long end = System.nanoTime();
       costTime = (end-start);
       //第一波輸出
       System.out.println("AtomicIntegerWithLock result:"+lockTestCase.get()+" costTime:"+costTime);
       System.out.println("=======我是分割線=======");
       //第二波資料準備
       threads= new Thread[threadCount];
       prepare(threads, synchronizedtestCase, threadCount, loopCount);
       //第二波啟動
       costTime = 0;
       start = System.nanoTime();
       startAndWait(threads);
       end = System.nanoTime();
       costTime = (end-start);
       //第二波輸出
       System.out.println("AtomicIntegerWithSynchronized result:"+lockTestCase.get()+" costTime:"+costTime);
    }
 
}

執行結果:
AtomicIntegerWithLock result:20000000 costTime:1192257757
=======我是分割線=======
AtomicIntegerWithSynchronized result:20000000 costTime:3955951264

這個例子很簡單,200個執行緒,每次執行10w++操作,很清楚的大家看到,最好的執行結果都是20000000,都能正常的保證資料的一致性,另外我們還能看到一點,就是Lock消耗的時間要比synchronized少,也證明了Lock的效能是要筆synchronized好的!

前面我們看到了Locksynchronized都能正常的保證資料的一致性(上文例子中執行的結果都是20000000),也看到了Lock的優勢,那究竟他們是什麼原理來保障的呢?今天我們就來探討下Java中的鎖機制!

Synchronized是基於JVM來保證資料同步的,而Lock則是在硬體層面,依賴特殊的CPU指令實現資料同步的,那究竟是如何來實現的呢?我們一一看來!

一、synchronized的實現方案

synchronized比較簡單,語義也比較明確,儘管Lock推出後效能有較大提升,但是基於其使用簡單,語義清晰明瞭,使用還是比較廣泛的,其應用層的含義是把任意一個非NULL的物件當作鎖。當synchronized作用於方法時,鎖住的是物件的例項(this),當作用於靜態方法時,鎖住的是Class例項,又因為Class的相關資料儲存在永久帶,因此靜態方法鎖相當於類的一個全域性鎖,當synchronized作用於一個物件例項時,鎖住的是對應的程式碼塊。在SunHotSpot JVM實現中,其實synchronized鎖還有一個名字:物件監視器。

當多個執行緒一起訪問某個物件監視器的時候,物件監視器會將這些請求儲存在不同的容器中。

1、Contention List:競爭佇列,所有請求鎖的執行緒首先被放在這個競爭佇列中

2、Entry ListContention List中那些有資格成為候選資源的執行緒被移動到Entry List

3、Wait Set:哪些呼叫wait方法被阻塞的執行緒被放置在這裡

4、OnDeck:任意時刻,最多隻有一個執行緒正在競爭鎖資源,該執行緒被成為OnDeck

5、Owner:當前已經獲取到所資源的執行緒被稱為Owner

6、!Owner:當前釋放鎖的執行緒

下圖展示了他們之前的關係


ContentionList

並不是真正意義上的一個佇列。僅僅是一個虛擬佇列,它只有Node以及對應的Next指標構成,並沒有Queue的資料結構。每次新加入Node會在隊頭進行,通過CAS改變第一個節點為新增節點,同時新增階段的next指向後續節點,而取資料都在佇列尾部進行。


 JVM

每次從佇列的尾部取出一個數據用於鎖競爭候選者(OnDeck),但是併發情況下,ContentionList會被大量的併發執行緒進行CAS訪問,為了降低對尾部元素的競爭,JVM會將一部分執行緒移動到EntryList中作為候選競爭執行緒。Owner執行緒會在unlock時,將ContentionList中的部分執行緒遷移到EntryList中,並指定EntryList中的某個執行緒為OnDeck執行緒(一般是最先進去的那個執行緒)。Owner執行緒並不直接把鎖傳遞給OnDeck執行緒,而是把鎖競爭的權利交個OnDeckOnDeck需要重新競爭鎖。這樣雖然犧牲了一些公平性,但是能極大的提升系統的吞吐量,在JVM中,也把這種選擇行為稱之為“競爭切換”。

OnDeck執行緒獲取到鎖資源後會變為Owner執行緒,而沒有得到鎖資源的仍然停留在EntryList中。如果Owner執行緒被wait方法阻塞,則轉移到WaitSet佇列中,直到某個時刻通過notify或者notifyAll喚醒,會重新進去EntryList中。

處於ContentionListEntryListWaitSet中的執行緒都處於阻塞狀態,該阻塞是由作業系統來完成的(Linux核心下采用pthread_mutex_lock核心函式實現的)。該執行緒被阻塞後則進入核心排程狀態,會導致系統在使用者和核心之間進行來回切換,嚴重影響鎖的效能。為了緩解上述效能問題,JVM引入了自旋鎖。原理非常簡單,如果Owner執行緒能在很短時間內釋放鎖資源,那麼哪些等待競爭鎖的執行緒可以稍微等一等(自旋)而不是立即阻塞,當Owner執行緒釋放鎖後可立即獲取鎖,進而避免使用者執行緒和核心的切換。但是Owner可能執行的時間會超過設定的閾值,爭用執行緒在一定時間內還是獲取不到鎖,這是爭用執行緒會停止自旋進入阻塞狀態。基本思路就是先自旋等待一段時間看能否成功獲取,如果不成功再執行阻塞,儘可能的減少阻塞的可能性,這對於佔用鎖時間比較短的程式碼塊來說效能能大幅度的提升!

但是有個頭大的問題,何為自旋?其實就是執行幾個空方法,稍微等一等,也許是一段時間的迴圈,也許是幾行空的彙編指令,其目的是為了佔著CPU的資源不釋放,等到獲取到鎖立即進行處理。但是如何去選擇自旋的執行時間呢?如果自旋執行時間太長,會有大量的執行緒處於自旋狀態佔用CPU資源,進而會影響整體系統的效能。因此自旋的週期選的額外重要!

JVM對於自旋週期的選擇,基本認為一個執行緒上下文切換的時間是最佳的一個時間,同時JVM還針對當前CPU的負荷情況做了較多的優化

1、如果平均負載小於CPUs則一直自旋

2、如果有超過(CPUs/2)個執行緒正在自旋,則後來執行緒直接阻塞

3、如果正在自旋的執行緒發現Owner發生了變化則延遲自旋時間(自旋計數)或進入阻塞

4、如果CPU處於節電模式則停止自旋

5、自旋時間的最壞情況是CPU的儲存延遲(CPU A儲存了一個數據,到CPU B得知這個資料直接的時間差)

6、自旋時會適當放棄執行緒優先順序之間的差異

Synchronized線上程進入ContentionList時,等待的執行緒就通過自旋先獲取鎖,如果獲取不到就進入ContentionList,這明顯對於已經進入佇列的執行緒是不公平的,還有一個不公平的事情就是自旋獲取鎖的執行緒還可能直接搶佔OnDeck執行緒的鎖資源。

JVM6以後還引入了一種偏向鎖,主要用於解決無競爭下面鎖的效能問題。我們首先來看沒有這個會有什麼樣子的問題。

現在基本上所有的鎖都是可重入的,即已經獲取鎖的執行緒可以多次鎖定/解鎖監視物件,但是按照之前JVM的設計,每次加鎖解鎖都採用CAS操作,而CAS會引發本地延遲(下面會講原因),因此偏向鎖希望執行緒一旦獲取到監視物件後,之後讓監視物件偏向這個鎖,進而避免多次CAS操作,說白了就是設定了一個變數,發現是這個執行緒過來的就避免再走加鎖解鎖流程。

CAS為什麼會引發本地延遲呢?這要從多核處(SMP)理架構說起(前面有提到過--),下圖基本上表明瞭多核處理的架構


多核

CPU會共享一條系統匯流排,靠匯流排和主存通訊,但是每個CPU又有自己的一級快取,而CAS是一條原子指令,其作用是讓CPU比較,如果相同則進行資料更新,而這些是基於硬體實現的(JVM只是封裝了硬體的彙編呼叫,AtomicInteger其實是通過呼叫這些封裝後的介面實現的)。多核運算時,由於執行緒切換,很有可能第二次取值是在另外一核CPU上執行的。假設Core1Core2把對應的某個值載入到自己的一級快取時,某個時刻,core1更新了這個資料並通過匯流排通知主存,此時core2的一級快取中的資料就失效了,他需要從主存中重新載入一次到一級快取中,大家通過匯流排通訊被稱之為一致性流量,匯流排的通訊能力有限,當快取一致性流量過大時,匯流排會成為瓶頸,而當Core1

相關推薦

深入淺出Java併發(Lock)VS同步(synchronized)

今天我們來探討一下Java中的鎖機制。前面我們提到,在JDK1.5之前只能通過synchronized關鍵字來實現同步,這個前面我們已經提到是屬於獨佔鎖,效能並不高,因此JDK1.5之後開始藉助JNI實現了一套高效的鎖實現! JDK5以後鎖的介面是JUC中的Lock

Java併發原始碼學習之AQS框架(二)CLH lock queue和自旋

上一篇文章提到AQS是基於CLH lock queue,那麼什麼是CLH lock queue,說複雜很複雜說簡單也簡單, 所謂大道至簡: CLH lock queue其實就是一個FIFO的佇列,佇列中的每個結點(執行緒)只要等待其前繼釋放鎖就可以了。 AbstractQueuedSynchronizer

java併發&執行緒池原理分析&的深度化

將不安全的map集合轉成安全集合 HashMap HashMap = new HashMap<>(); Collections.synchronizedMap(HashMap); concurrentHasMap 開發推薦使用這種map———執行

(2.1.27.11)Java併發程式設計:Lock之ReentrantReadWriteLock 讀寫分離獨享式重入

我們在介紹AbstractQueuedSynchronizer的時候介紹過,AQS支援獨佔式同步狀態獲取/釋放、共享式同步狀態獲取/釋放兩種模式,對應的典型應用分別是ReentrantLock和Semaphore AQS還可以混合兩種模式使用,讀寫鎖Reent

(2.1.27.13)Java併發程式設計:Lock之CountDownLatch計數式獨享

CountDownLatch是一種java.util.concurrent包下一個同步工具類,它允許一個或多個執行緒等待直到在其他執行緒中一組操作執行完成。 相對於前文的鎖,它主要實現了: 呼叫指定次release後,才會釋放鎖 一、使用 public st

java 併發同步 CountDownLatch, CyclicBarrier, Semaphore

java 執行緒併發包 通常為java.util.concurrent 下的包 執行緒包提供的同步結構主要有三個 CountDownLatch CyclicBarrier Semaphore C

Java併發之閉鎖/柵欄/訊號量及併發模型和

threadLocal能夠為每一個執行緒維護變數副本,常用於在多執行緒中用空間換時間     程序死鎖:程序死鎖,指多個程序迴圈等待他方佔有的資源而一直等待下去的局面;  程序活鎖:執行緒1,2需要同時佔有a,b才可以,1佔有a,2佔有b,為了避免死鎖,

Java併發下的(4)——Condition介面

Condition 介面提供了類似Object的監視器方法,與Lock配合可以實現 等待/通知 模式 Condition的介面 在說Condition的介面之前,先對比一下與Object監視器的異同: 對比項 Object的監視器(Monitor)

Java併發程式設計的藝術》-Java併發中的讀寫及其實現分析

1. 前言 在Java併發包中常用的鎖(如:ReentrantLock),基本上都是排他鎖,這些鎖在同一時刻只允許一個執行緒進行訪問,而讀寫鎖在同一時刻可以允許多個讀執行緒訪問,但是在寫執行緒訪問時,所有的讀執行緒和其他寫執行緒均被阻塞。讀寫鎖維護了一對鎖,一個讀鎖和一個寫鎖,通過分離讀鎖和

Java併發中的同步佇列SynchronousQueue實現原理

作者:一粟 介紹 Java 6的併發程式設計包中的SynchronousQueue是一個沒有資料緩衝的BlockingQueue,生產者執行緒對其的插入操作put必須等待消費者的移除操作take,反過來也一樣。 不像ArrayBlockingQueue或LinkedListBlockingQu

深入淺出Java Concurrent,Java併發整理

最近整理了一下java.util.concurrrent包下的相關類和功能實現。把相關比較好的部落格推薦一下給大家 先看一下JUC的大體結構 ReentrantLock實現原理深入探究 ConcurrentSkipList實現原理 :SkipList 跳錶 Conc

Java併發中共享的實現原理

共享鎖--用到共享鎖的有Semapore訊號量和ReadLock讀鎖 共享式獲取和獨佔式獲取最主要的區別就是:在同一時刻能否有多個執行緒同時獲取到同步狀態。 下面我們分析下原始碼: 我們以Semaphore為例。Semaphore是訊號量,---作用就是一個共享資源

Java併發程式設計:Lock(比synchronized更靈活的同步)

Lock 是 java.util.concurrent.locks 包下的介面,Lock  實現提供了比使用 synchronized 方法和語句可獲得的更廣泛的鎖定操作,它能以更優雅的方式處理執行緒同步問題,我們拿Java執行緒(二)中的一個例子簡單的實現一下和 s

Java併發原始碼學習系列:CLH同步佇列及同步資源獲取與釋放

[toc] ## 本篇學習目標 - 回顧CLH同步佇列的結構。 - 學習獨佔式資源獲取和釋放的流程。 ## CLH佇列的結構 我在[Java併發包原始碼學習系列:AbstractQueuedSynchronizer#同步佇列與Node節點](https://www.cnblogs.com/summer

Java併發原始碼學習系列:同步元件CountDownLatch原始碼解析

[toc] ## CountDownLatch概述 日常開發中,經常會遇到類似場景:主執行緒開啟多個子執行緒執行任務,需要等待所有子執行緒執行完畢後再進行彙總。 在同步元件CountDownLatch出現之前,我們可以使用join方法來完成,簡單實現如下: ```java public class J

Java併發原始碼學習系列:同步元件Semaphore原始碼解析

[toc] ## Semaphore概述及案例學習 Semaphore訊號量用來**控制同時訪問特定資源的執行緒數量**,它通過協調各個執行緒,以保證合理地使用公共資源。 ```java public class SemaphoreTest { private static final int

cocurrent Lock

write clipboard 通過 try data 進行 被鎖 這一 行鎖 20. 鎖 Lock java.util.concurrent.locks.Lock 是一個類似於 synchronized 塊的線程同步機制。但是 Lock 比 synchronized 塊

【程式設計架構實戰】——Java併發基石-AQS詳解

目錄 1 基本實現原理 1.1 如何使用 1.2 設計思想 2 自定義同步器 2.1 同步器程式碼實現 2.2 同步器程式碼測試 3 原始碼分析 3.1 Node結點 3.2 獨佔式 3.3 共享式 4 總結   Java併發包(JUC)中提供了很多

深入java併發原始碼(一)簡介

閱讀本文章前需要了解 CAS 操作是什麼。 首先大致介紹一下需要講到的幾個類,只需要理解這幾個類是什麼關係即可,後面會有詳細解析。 Unsafe :這個類提供了 native 方法,未開源,提供了執行緒阻塞和喚醒,原子操作等方法。 LockSupport :包裝了一層 Unsafe 類,非常類似於代

Java併發(十三):同步屏障CyclicBarrier

CyclicBarrier 的字面意思是可迴圈使用(Cyclic)的屏障(Barrier)。它要做的事情是,讓一組執行緒到達一個屏障(也可以叫同步點)時被阻塞,直到最後一個執行緒到達屏障時,屏障才會開門,所有被屏障攔截的執行緒才會繼續幹活。 一、應用舉例 public class CyclicBar