java中的”鎖”事

NO IMAGE

前言

在說到Java鎖之前,先回顧一下Java異步編程中的多線程。

維基百科–>多線程(英語:multithreading),是指從軟件或者硬件上實現多個線程併發執行的技術。具有多線程能力的計算機因有硬件支持而能夠在同一時間執行多於一個線程,進而提升整體處理性能。具有這種能力的系統包括對稱多處理機多核心處理器以及芯片級多處理(Chip-level multithreading)或同時多線程(Simultaneous multithreading)處理器。

在我們代碼程序中,將一個程序分割成相互獨立的區域,或者將一個程序轉換成多個獨立運行的子任
務。像這樣的每個子任務把它叫作一個“線程”( Thread)。 有了多線程做開頭,接下來歸納下Java中的鎖。

Java提供了種類豐富的鎖,每種鎖因其特性的不同,在適當的場景下能夠展現出非常高的效率。本文旨在對鎖相關源碼(本文中的源碼來自JDK 8)、使用場景進行整理,作為一個簡單的總結吧。

Java中往往是按照是否含有某一特性來定義鎖,然後通過特性將鎖進行分組歸類,下面給出本文內容的總體分類目錄:

java中的

根據如上圖所示,大概Java中鎖就有這幾種:

  • 公平鎖/非公平鎖
  • 可重入鎖
  • 獨享鎖/共享鎖
  • 互斥鎖/讀寫鎖
  • 樂觀鎖/悲觀鎖
  • 分段鎖
  • 偏向鎖/輕量級鎖/重量級鎖
  • 自旋鎖

這些分類並不是全是指鎖的狀態,有的指鎖的特性,有的指鎖的設計。

1. 樂觀鎖 VS 悲觀鎖

樂觀鎖與悲觀鎖是一種宏觀上的概念,體現了看待線程同步的不同角度。在Java和數據庫中(MySQL中的鎖)都有此概念對應的實際應用。

悲觀鎖認為對於同一個數據的併發操作,一定是會發生修改的,哪怕沒有修改,也會認為修改。因此對於同一個數據的併發操作,悲觀鎖採取加鎖的形式。悲觀的認為,不加鎖的併發操作一定會出問題。
樂觀鎖則認為對於同一個數據的併發操作,是不會發生修改的。在更新數據的時候,會採用嘗試更新,不斷重新的方式更新數據。樂觀的認為,不加鎖的併發操作是沒有事情的。

Java中,synchronized關鍵字和Lock的實現類都是悲觀鎖。而樂觀鎖在Java中是通過使用無鎖編程來實現,最常採用的是CAS算法,Java原子類中的遞增操作就通過CAS自旋實現的。

從上面的描述我們可以看出,悲觀鎖適合寫操作非常多的場景,先加鎖可以保證寫操作時數據正確,而樂觀鎖適合讀操作非常多的場景,不加鎖會帶來大量的性能提升。
結合代碼來看下樂觀鎖和悲觀鎖的調用方式示例:

1.悲觀鎖的倆種調用方式

synchronized實現

java中的

lock實現

private static Lock lock = new ReentrantLock();

java中的

2.樂觀鎖調用方式

public class AtomicIntegerTest {
private static AtomicInteger n2 = new AtomicInteger(0);
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread() {
public void run() {
for(int i = 0; i < 1000; i++) {
n2.incrementAndGet();
}
}; 
};
Thread t2 = new Thread() {
public void run() {
for(int i = 0; i< 1000; i++) {
n2.incrementAndGet();
}
}
};
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println("最終n2的值為:" + n2.toString());
}
}

多次運行,發現結果永遠是2000,由此可以證明AtomicInteger的操作是原子性的。

看完上述試例,我們可以發現悲觀鎖基本都是在顯式的鎖定之後再操作同步資源,而樂觀鎖則直接去操作同步資源。那麼,為何樂觀鎖能夠做到不鎖定同步資源也可以正確的實現線程同步呢?樂觀鎖貌似是直接利用原子類的自增屬性執行自增1,類似於MySQL中的樂觀鎖實現方案版本號加1,這種實現方法就是 “CAS”技術。先介紹一下CAS:

CAS全稱 Compare And Swap(比較與交換),是一種無鎖算法。在不使用鎖(沒有線程被阻塞)的情況下實現多線程之間的變量同步。java.util.concurrent包中的原子類就是通過CAS來實現了樂觀鎖。

CAS算法涉及到三個操作數:

  • 需要讀寫的內存值 V。

  • 進行比較的值 A。

  • 要寫入的新值 B。

當且僅當 V 的值等於 A 時,CAS通過原子方式用新值B來更新V的值(“比較+更新”整體是一個原子操作),否則不會執行任何操作。一般情況下,“更新”是一個不斷重試的操作。這個和之前記錄的那篇文章(MySQL中的鎖)中的樂觀鎖原理基本是一樣的,理解起來也容易。
然後在上面提到java.util.concurrent包中的原子類AtomicInteger,就是通過CAS來實現了樂觀鎖,那麼我們進入原子類AtomicInteger的源碼,看一下AtomicInteger的定義:

public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;

以上為AtomicInteger中的部分源碼,在這裡說下其中的value,這裡value使用了volatile關鍵字,volatile的作用是使得多個線程可以共享變量,但是問題在於使用volatile將使得JVM優化失去作用,導致效率較低。
valueOffset是存儲value在AtomicInteger中的偏移量。
那麼unsafe類是什麼東西呢?

Unsafe類是在sun.misc包下,不屬於Java標準。但是很多Java的基礎類庫,包括一些被廣泛使用的高性能開發庫都是基於Unsafe類開發的,比如Netty、Cassandra、Hadoop、Kafka等。Unsafe類在提升Java運行效率,增強Java語言底層操作能力方面起了很大的作用。Unsafe類使Java擁有了像C語言的指針一樣操作內存空間的能力,同時也帶來了指針的問題。過度的使用Unsafe類會使得出錯的機率變大,因此Java官方並不建議使用的,官方文檔也幾乎沒有。通常我們最好也不要使用Unsafe類,除非有明確的目的,並且也要對它有深入的瞭解才行。

接下來,我們看下AtomicInteger的自增函數incrementAndGet()的源碼,發現自增函數底層調用的是unsafe.getAndAddInt(),然後找到getAndAddInt的源碼:

/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
  public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
  • 其中getIntVolatile和compareAndSwapInt都是native方法。
  • getIntVolatile是獲取當前的期望值。
  • compareAndSwapInt就是我們平時說的CAS(compare and。 swap),通過比較如果內存區的值沒有改變,那麼就用新值直接給該內存區賦值。

所以incrementAndGet是將自增後的值返回,相當於++i,而getAndAddInt()循環獲取給定對象o中的偏移量處的值v,然後判斷內存值是否等於v。如果相等則將內存值設置為 v + delta,否則返回false,繼續循環進行重試,直到設置成功才能退出循環,並且將舊值返回。整個“比較+更新”操作封裝在compareAndSwapInt()中,屬於原子操作,可以保證多個線程都能夠看到同一個變量的修改值。

這裡就順便把AtomicInteger原子類也整理了下。

2. 自旋鎖

在Java中,自旋鎖是指嘗試獲取鎖的線程不會立即阻塞,而是採用循環的方式去嘗試獲取鎖,這樣的好處是減少線程上下文切換的消耗,缺點是循環會消耗CPU。

自旋等待雖然避免了線程切換的開銷,但它要佔用處理器時間。如果鎖被佔用的時間很短,自旋等待的效果就會非常好。反之,如果鎖被佔用的時間很長,那麼自旋的線程只會白浪費處理器資源。所以,自旋等待的時間必須要有一定的限度,如果自旋超過了限定次數(默認是10次,可以使用-XX:PreBlockSpin來更改)沒有成功獲得鎖,就應當掛起線程。

自旋鎖的實現原理同樣也是CAS,下面代碼中的while循環就是一個自旋操作:

 public class SpinLock {
private AtomicReference<Thread> sign =new AtomicReference<>();
public void lock(){
Thread current = Thread.currentThread();
while(!sign .compareAndSet(null, current)){
}
}
public void unlock (){
Thread current = Thread.currentThread();
sign .compareAndSet(current, null);
}
}

自旋鎖在JDK1.4.2中引入,使用-XX:+UseSpinning來開啟。JDK 6中變為默認開啟,並且引入了自適應的自旋鎖(適應性自旋鎖)。

自適應意味著自旋的時間(次數)不再固定,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。

3. 無鎖 VS 偏向鎖 VS 輕量級鎖 VS 重量級鎖

java中的

這四種鎖是根據狀態去分別的,並且是針對Synchronized,在Java 5通過引入鎖升級的機制來實現高效Synchronized。這三種鎖的狀態是通過對象監視器在對象頭中的字段(Mark Word)來表明的。目前鎖一共有這4種狀態,級別從低到高依次是:無鎖、偏向鎖、輕量級鎖和重量級鎖。鎖狀態只能升級不能降級。

無鎖沒有對資源進行鎖定,所有的線程都能訪問並修改同一個資源,但同時只有一個線程能修改成功。也就是說要保證安全線程,並不是一定就要進行加鎖,兩者沒有因果關係。加鎖只是保證共享數據爭用時的正確性的手段,如果一個方法本來就不涉及共享數據,那它自然就無須任何同步措施去保證正確性,因此會有一些代碼天生就是線程安全的。

偏向鎖是指一段同步代碼一直被一個線程所訪問,那麼該線程會自動獲取鎖。降低獲取鎖的代價。

輕量級鎖是指當鎖是偏向鎖的時候,被另一個線程所訪問,偏向鎖就會升級為輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,提高性能。

重量級鎖是指當鎖為輕量級鎖的時候,另一個線程雖然是自旋,但自旋不會一直持續下去,當自旋一定次數的時候,還沒有獲取到鎖,就會進入阻塞,該鎖膨脹為重量級鎖。重量級鎖會讓其他申請的線程進入阻塞,性能降低。

偏向鎖、輕量級鎖以及重量級鎖是通過Java對象頭實現的。

4. 公平鎖 VS 非公平鎖

公平鎖是指多個線程按照申請鎖的順序來獲取鎖,線程直接進入隊列中排隊,隊列中的第一個線程才能獲得鎖。

非公平鎖是指多個線程獲取鎖的順序並不是按照申請鎖的順序,有可能後申請的線程比先申請的線程優先獲取鎖。有可能,會造成優先級反轉或者飢餓現象。

對於Synchronized而言,也是一種非公平鎖。由於其並不像ReentrantLock是通過AQS的來實現線程調度,所以並沒有任何辦法使其變成公平鎖。

那如何能保證每個線程都能拿到鎖呢,隊列FIFO是一個完美的解決方案,也就是先進先出,ReenTrantLock通過構造函數指定該鎖是否是公平鎖,默認是非公平鎖。非公平鎖的優點在於吞吐量比公平鎖大。所以ReenTrantLock是根據隊列實現的公平鎖和非公平鎖。

公平鎖:

        final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
#hasQueuedPredecessors的實現
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}

非公平鎖:

        final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}

通過源碼對比,我們可以明顯的看出公平鎖與非公平鎖的lock()方法唯一的區別就在於公平鎖在獲取同步狀態時多了一個限制條件:hasQueuedPredecessors()。
再進入hasQueuedPredecessors(),可以看到該方法主要做一件事情:主要是判斷當前線程是否位於同步隊列中的第一個。如果是則返回true,否則返回false。

5. 可重入鎖 VS 非可重入鎖

可重入鎖又名遞歸鎖,是指在同一個線程在外層方法獲取鎖的時候,在進入內層方法會自動獲取鎖。說的有點抽象,下面會有一個代碼的示例。

對於Java ReentrantLock而言, 他的名字就可以看出是一個可重入鎖,其名字是Re entrant Lock重新進入鎖。

對於Synchronized而言,也是一個可重入鎖。可重入鎖的一個好處是可一定程度避免死鎖。

synchronized void setA() throws Exception{
Thread.sleep(1000);
setB();
}
synchronized void setB() throws Exception{
Thread.sleep(1000);
}

上面的代碼就是一個可重入鎖的一個特點,如果不是可重入鎖的話,setB可能不會被當前線程執行,可能造成死鎖。

6. 獨享鎖 VS 共享鎖

獨享鎖也叫排他鎖或者互斥鎖是指該鎖一次只能被一個線程所持有。

共享鎖是指該鎖可被多個線程所持有。

比如,ReentrantLock 是互斥鎖,ReentrantReadWriteLock 中的寫鎖是也是互斥鎖
JDK中的synchronized也是互斥鎖。
但是ReentrantReadWriteLock 中的讀鎖就是共享鎖。
最後再根據ReentrantReadWriteLock的源碼看看獨享鎖和共享鎖:

public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
private static final long serialVersionUID = -6992448646407690164L;
/** Inner class providing readlock */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;
/** Performs all synchronization mechanics */
final Sync sync;
/**
* Creates a new {@code ReentrantReadWriteLock} with
* default (nonfair) ordering properties.
*/
public ReentrantReadWriteLock() {
this(false);
}
/**
* Creates a new {@code ReentrantReadWriteLock} with
* the given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock  readLock()  { return readerLock; }
/**

我們看到ReentrantReadWriteLock有兩把鎖:ReadLock和WriteLock,由詞知意,這倆把鎖就是上文提到的一個讀鎖一個寫鎖,合稱“讀寫鎖”。

然後點進去發現ReadLock和WriteLock是靠內部類Sync實現的鎖,但讀鎖和寫鎖的加鎖方式不一樣。讀鎖是共享鎖,寫鎖是獨享鎖。讀鎖的共享鎖可保證併發讀非常高效,而讀寫、寫讀、寫寫的過程互斥,因為讀鎖和寫鎖是分離的。所以ReentrantReadWriteLock的併發性相比一般的互斥鎖有了很大提升。

倆者具體的區別就是寫鎖加鎖的tryAcquire()和讀鎖加鎖的tryAcquireShared()的區別,後面再研究下。

限於篇幅以及個人水平就先總結到這吧。

相關文章

說說Flutter中的Semantics

這一次,徹底理解https原理

那天晚上和@FeignClient註解的深度交流

GraphQL入門到實踐