Java基礎——深入理解ReentrantLock

一、簡介


? ? ? ?在Java中通常實現鎖有兩種方式,一種是synchronized關鍵字,另一種是Lock。二者其實并沒有什么必然聯系,但是各有各的特點,在使用中可以進行取舍的使用。


二、ReentrantLock與synchronized的比較


相同點:

(1)ReentrantLock提供了synchronized類似的功能和內存語義。


不同點:

? ? (1)ReentrantLock功能性方面更全面,比如定時等候鎖、可中斷鎖等候、鎖投票等,因此更有擴展性。在多個條件變量和高度競爭鎖的地方,用ReentrantLock更合適,ReentrantLock還提供了Condition,對線程的等待和喚醒等操作更加靈活,一個ReentrantLock可以有多個Condition實例,所以更有擴展性。

? ? (2)ReentrantLock 的性能比synchronized會好點。

? ? (3)ReentrantLock提供了可輪詢的鎖請求,他可以嘗試的去取得鎖,如果取得成功則繼續處理,取得不成功,可以等下次運行的時候處理,所以不容易產生死鎖,而synchronized則一旦進入鎖請求要么成功,要么一直阻塞,所以更容易產生死鎖。

? ? (4)對于使用者的直觀體驗上Lock是比較復雜的,需要lock和realse,通常需要在finally中進行鎖的釋放,否則,如果受保護的代碼將拋出異常,就會產生死鎖的問題,這一點區別看起來可能沒什么,但是實際上,它極為重要。但是synchronized的使用十分簡單,只需要對自己的方法或者關注的同步對象或類使用synchronized關鍵字即可,JVM 將確保鎖會獲得自動釋放。但是對于鎖的粒度控制比較粗,同時對于實現一些鎖的狀態的轉移比較困難。

? ? (5) 當 JVM 用 synchronized 管理鎖定請求和釋放時,JVM 在生成線程轉儲時能夠包括鎖定信息。這些對調試非常有價值,因為它們能標識死鎖或者其他異常行為的來源。 Lock 類只是普通的類,JVM 不知道具體哪個線程擁有 Lock 對象。


三、ReentrantLock


? ? ? ?java.util.concurrent.lock 中的 Lock 框架是鎖定的一個抽象,它允許把鎖定的實現作為 Java 類,而不是作為語言的特性來實現。這就為 Lock 的多種實現留下了空間,各種實現可能有不同的調度算法、性能特性或者鎖定語義。 ReentrantLock 類實現了 Lock ,它擁有與 synchronized 相同的并發性和內存語義,但是添加了類似鎖投票、定時鎖等候和可中斷鎖等候的一些特性。此外,它還提供了在激烈爭用情況下更佳的性能。(換句話說,當許多線程都想訪問共享資源時,JVM 可以花更少的時候來調度線程,把更多時間用在執行線程上。)
? ? ? ?reentrant 鎖意味著什么呢?簡單來說,它有一個與鎖相關的獲取計數器,如果擁有鎖的某個線程再次得到鎖,那么獲取計數器就加1,然后鎖需要被釋放兩次才能獲得真正釋放。這模仿了 synchronized 的語義;如果線程進入由線程已經擁有的監控器保護的 synchronized 塊,就允許線程繼續進行,當線程退出第二個(或者后續) synchronized 塊的時候,不釋放鎖,只有線程退出它進入的監控器保護的第一個 synchronized 塊時,才釋放鎖。

1、實現可輪詢的鎖請求
? ? ? ?在內部鎖中,死鎖是致命的——唯一的恢復方法是重新啟動程序,唯一的預防方法是在構建程序時不要出錯。而可輪詢的鎖獲取模式具有更完善的錯誤恢復機制,可以規避死鎖的發生。?
? ? ? ?如果你不能獲得所有需要的鎖,那么使用可輪詢的獲取方式使你能夠重新拿到控制權,它會釋放你已經獲得的這些鎖,然后再重新嘗試。可輪詢的鎖獲取模式,由tryLock()方法實現。此方法僅在調用時鎖為空閑狀態才獲取該鎖。如果鎖可用,則獲取鎖,并立即返回值true。如果鎖不可用,則此方法將立即返回值false。此方法的典型使用語句如下:?

Lock lock = ...;   
if (lock.tryLock()) {   try {   // manipulate protected state   } finally {   lock.unlock();   }   
} else {   // perform alternative actions   
}   

2、實現可定時的鎖請求
? ? ? ?當使用內部鎖時,一旦開始請求,鎖就不能停止了,所以內部鎖給實現具有時限的活動帶來了風險。為了解決這一問題,可以使用定時鎖。當具有時限的活?動調用了阻塞方法,定時鎖能夠在時間預算內設定相應的超時。如果活動在期待的時間內沒能獲得結果,定時鎖能使程序提前返回。可定時的鎖獲取模式,由tryLock(long, TimeUnit)方法實現。?

3、實現可中斷的鎖獲取請求?
? ? ? ?可中斷的鎖獲取操作允許在可取消的活動中使用。lockInterruptibly()方法能夠使你獲得鎖的時候響應中斷。

四、條件變量Condition

? ? ? ?條件變量很大一個程度上是為了解決Object.wait/notify/notifyAll難以使用的問題。

? ? ? ?條件(也稱為條件隊列?或條件變量)為線程提供了一個含義,以便在某個狀態條件現在可能為 true 的另一個線程通知它之前,一直掛起該線程(即讓其“等待”)。因為訪問此共享狀態信息發生在不同的線程中,所以它必須受保護,因此要將某種形式的鎖與該條件相關聯。等待提供一個條件的主要屬性是:以原子方式?釋放相關的鎖,并掛起當前線程,就像?Object.wait?做的那樣。

? ? ? ?上述API說明表明條件變量需要與鎖綁定,而且多個Condition需要綁定到同一鎖上。前面的Lock中提到,獲取一個條件變量的方法是Lock.newCondition()。

void await() throws InterruptedException;  void awaitUninterruptibly();  long awaitNanos(long nanosTimeout) throws InterruptedException;  boolean await(long time, TimeUnit unit) throws InterruptedException;  boolean awaitUntil(Date deadline) throws InterruptedException;  void signal();  void signalAll(); 

? ? ? ?以上是Condition接口定義的方法,await*對應于Object.wait,signal對應于Object.notify,signalAll對應于Object.notifyAll。特別說明的是Condition的接口改變名稱就是為了避免與Object中的wait/notify/notifyAll的語義和使用上混淆,因為Condition同樣有wait/notify/notifyAll方法

? ? ? ?每一個Lock可以有任意數據的Condition對象,Condition是與Lock綁定的,所以就有Lock的公平性特性:如果是公平鎖,線程為按照FIFO的順序從Condition.await中釋放,如果是非公平鎖,那么后續的鎖競爭就不保證FIFO順序了。

一個使用Condition實現生產者消費者的模型例子如下。

import java.util.concurrent.locks.Condition;  
import java.util.concurrent.locks.Lock;  
import java.util.concurrent.locks.ReentrantLock;  public class ProductQueue<T> {  private final T[] items;  private final Lock lock = new ReentrantLock();  private Condition notFull = lock.newCondition();  private Condition notEmpty = lock.newCondition();  //  private int head, tail, count;  public ProductQueue(int maxSize) {  items = (T[]) new Object[maxSize];  }  public ProductQueue() {  this(10);  }  public void put(T t) throws InterruptedException {  lock.lock();  try {  while (count == getCapacity()) {  notFull.await();  }  items[tail] = t;  if (++tail == getCapacity()) {  tail = 0;  }  ++count;  notEmpty.signalAll();  } finally {  lock.unlock();  }  }  public T take() throws InterruptedException {  lock.lock();  try {  while (count == 0) {  notEmpty.await();  }  T ret = items[head];  items[head] = null;//GC  //  if (++head == getCapacity()) {  head = 0;  }  --count;  notFull.signalAll();  return ret;  } finally {  lock.unlock();  }  }  public int getCapacity() {  return items.length;  }  public int size() {  lock.lock();  try {  return count;  } finally {  lock.unlock();  }  }  }  

? ? ? ?在這個例子中消費take()需要 隊列不為空,如果為空就掛起(await()),直到收到notEmpty的信號;生產put()需要隊列不滿,如果滿了就掛起(await()),直到收到notFull的信號。

? ? ? ?可能有人會問題,如果一個線程lock()對象后被掛起還沒有unlock,那么另外一個線程就拿不到鎖了(lock()操作會掛起),那么就無法通知(notify)前一個線程,這樣豈不是“死鎖”了?


1、await* 操作

? ? ? ?上一節中說過多次ReentrantLock是獨占鎖,一個線程拿到鎖后如果不釋放,那么另外一個線程肯定是拿不到鎖,所以在lock.lock()lock.unlock()之間可能有一次釋放鎖的操作(同樣也必然還有一次獲取鎖的操作)。我們再回頭看代碼,不管take()還是put(),在進入lock.lock()后唯一可能釋放鎖的操作就是await()了。也就是說await()操作實際上就是釋放鎖,然后掛起線程,一旦條件滿足就被喚醒,再次獲取鎖!

public final void await() throws InterruptedException {  if (Thread.interrupted())  throw new InterruptedException();  Node node = addConditionWaiter();  int savedState = fullyRelease(node);  int interruptMode = 0;  while (!isOnSyncQueue(node)) {  LockSupport.park(this);  if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)  break;  }  if (acquireQueued(node, savedState) && interruptMode != THROW_IE)  interruptMode = REINTERRUPT;  if (node.nextWaiter != null)  unlinkCancelledWaiters();  if (interruptMode != 0)  reportInterruptAfterWait(interruptMode);  
}  

? ? ? ?上面是await()的代碼片段。上一節中說過,AQS在獲取鎖的時候需要有一個CHL的FIFO隊列,所以對于一個Condition.await()而言,如果釋放了鎖,要想再一次獲取鎖那么就需要進入隊列,等待被通知獲取鎖。完整的await()操作是安裝如下步驟進行的:

  1. 將當前線程加入Condition鎖隊列。特別說明的是,這里不同于AQS的隊列,這里進入的是Condition的FIFO隊列。后面會具體談到此結構。進行2。
  2. 釋放鎖。這里可以看到將鎖釋放了,否則別的線程就無法拿到鎖而發生死鎖。進行3。
  3. 自旋(while)掛起,直到被喚醒或者超時或者CACELLED等。進行4。
  4. 獲取鎖(acquireQueued)。并將自己從Condition的FIFO隊列中釋放,表明自己不再需要鎖(我已經拿到鎖了)。

? ? ? ?這里再回頭介紹Condition的數據結構。我們知道一個Condition可以在多個地方被await*(),那么就需要一個FIFO的結構將這些Condition串聯起來,然后根據需要喚醒一個或者多個(通常是所有)。所以在Condition內部就需要一個FIFO的隊列。

private transient Node firstWaiter;  
private transient Node lastWaiter;  
? ? ? ?上面的兩個節點就是描述一個FIFO的隊列。我們再結合前面提到的節點(Node)數據結構。我們就發現Node.nextWaiter就派上用場了!nextWaiter就是將一系列的Condition.await*串聯起來組成一個FIFO的隊列。


2、signal/signalAll 操作


? ? ? ?await*()清楚了,現在再來看signal/signalAll就容易多了。按照signal/signalAll的需求,就是要將Condition.await*()中FIFO隊列中第一個Node喚醒(或者全部Node)喚醒。盡管所有Node可能都被喚醒,但是要知道的是仍然只有一個線程能夠拿到鎖,其它沒有拿到鎖的線程仍然需要自旋等待,就上上面提到的第4步(acquireQueued)。

private void doSignal(Node first) {  do {  if ( (firstWaiter = first.nextWaiter) == null)  lastWaiter = null;  first.nextWaiter = null;  } while (!transferForSignal(first) &&  (first = firstWaiter) != null);  
}  private void doSignalAll(Node first) {  lastWaiter = firstWaiter  = null;  do {  Node next = first.nextWaiter;  first.nextWaiter = null;  transferForSignal(first);  first = next;  } while (first != null);  
}  

? ? ? ?上面的代碼很容易看出來,signal就是喚醒Condition隊列中的第一個非CANCELLED節點線程,而signalAll就是喚醒所有非CANCELLED節點線程。當然了遇到CANCELLED線程就需要將其從FIFO隊列中剔除。

final boolean transferForSignal(Node node) {  if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))  return false;  Node p = enq(node);  int c = p.waitStatus;  if (c > 0 || !compareAndSetWaitStatus(p, c, Node.SIGNAL))  LockSupport.unpark(node.thread);  return true;  
}  
? ? ? ?上面就是喚醒一個 await*() 線程的過程,根據前面的小節介紹的,如果要 unpark 線程,并使線程拿到鎖,那么就需要線程節點進入 AQS 的隊列。所以可以看到在 LockSupport.unpark 之前調用了 enq(node) 操作,將當前節點加入到 AQS 隊列。



本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/446969.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/446969.shtml
英文地址,請注明出處:http://en.pswp.cn/news/446969.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

使用開源的openssl的md5頭文件,實現對于文件的md5代碼

需要安裝openssl的庫 sudo apt-get install opensslsudo apt-get install libssl-dev參考鏈接 代碼 #include "openssl/md5.h" #include <iostream> #include <fstream> #include <iomanip>//#define MAX_DATA_BUFF 1024; //#define MD5_LENGTH…

Android 多進程開發

前言正常情況下&#xff0c;一個apk啟動后只會運行在一個進程中&#xff0c;其進程名為AndroidManifest.xml文件中指定的應用包名&#xff0c;所有的基本組件都會在這個進程中運行。但是如果需要將某些組件&#xff08;如Service、Activity等&#xff09;運行在單獨的進程中&am…

clion中鏈接openssl庫

錯誤顯示 前提條件 apt-get install opensslapt-get install openssl-dev 解決辦法 在CMakeLists.txt文件中加入如下命令link_libraries(crypto) 參考鏈接 無法將openssl庫鏈接到CLion C 程序c - 無法將openssl庫鏈接到CLion C程序

Java中String、StringBuffer、StringBuilder三者的區別

一、簡介String、StringBuffer、StringBuilder三個類之間的區別主要是在兩個方面&#xff1a;運行速度和線程安全。二、區別1、運行速度&#xff0c;或者說是執行速度在這方面運行速度快慢為&#xff1a;StringBuilder > StringBuffer > String StringString為字符串常量…

Ubuntu環境下,使用clion編譯器,使用開源opensll的對稱AES算法對于文件進行加密,C++代碼

前提準備條件 需要安裝openssl需要安裝openssl-dev需要配置CMakeLists.txt文件集體內容可以參考我提供的相關參考鏈接 AES_file.h #include <openssl/aes.h> #include <iostream> #include <fstream> #include <cstring>#define RELEASE_ARRAY(P) if…

Java提高篇 —— Java關鍵字之static的四種用法

一、前言 在java的關鍵字中&#xff0c;static和final是兩個我們必須掌握的關鍵字。不同于其他關鍵字&#xff0c;他們都有多種用法&#xff0c;而且在一定環境下使用&#xff0c;可以提高程序的運行性能&#xff0c;優化程序的結構。下面我們先來了解一下static關鍵字及其用法…

C++ 使用move來刪除用戶指定的文件

代碼 #include <iostream>bool remove_file(std::string path){if (remove(path.c_str())0){std::cout << "success!" << std::endl;}else{std::cout << "False!" << std::endl;} } int main() {std::string path "/…

Java提高篇 —— Java關鍵字之final的幾種用法

一、前言 在java的關鍵字中&#xff0c;static和final是兩個我們必須掌握的關鍵字。不同于其他關鍵字&#xff0c;他們都有多種用法&#xff0c;而且在一定環境下使用&#xff0c;可以提高程序的運行性能&#xff0c;優化程序的結構。下面我們來了解一下final關鍵字及其用法。 …

使用C++的方式實現AES算法

aes_file.h #include <iostream> #include <fstream> #include <bitset> #include <string> using namespace std; typedef bitset<8> byte; typedef bitset<32> word;const int Nr 10; // AES-128需要 10 輪加密 const int Nk 4; /…

Java提高篇 —— Java三大特性之封裝

一、封裝 封裝從字面上來理解就是包裝的意思&#xff0c;專業點就是信息隱藏&#xff0c;是指利用抽象數據類型將數據和基于數據的操作封裝在一起&#xff0c;使其構成一個不可分割的獨立實體&#xff0c;數據被保護在抽象數據類型的內部&#xff0c;盡可能地隱藏內部的細節&am…

sqlite3的backup和restore函數的使用

參考代碼 第一段這個親測可以使用 #include <sqlite3.h> #include <iostream> /* ** Perform an online backup of database pDb to the database file named ** by zFilename. This function copies 5 database pages from pDb to ** zFilename, then unlocks pD…

Java提高篇 —— Java三大特性之繼承

一、前言 在《Think in java》中有這樣一句話&#xff1a;復用代碼是Java眾多引人注目的功能之一。但要想成為極具革命性的語言&#xff0c;僅僅能夠復制代碼并對加以改變是不夠的&#xff0c;它還必須能夠做更多的事情。在這句話中最引人注目的是“復用代碼”,盡可能的復用代碼…

Java提高篇 —— Java三大特性之多態

一、前言 面向對象編程有三大特性&#xff1a;封裝、繼承、多態。 封裝&#xff1a;隱藏了類的內部實現機制&#xff0c;可以在不影響使用的情況下改變類的內部結構&#xff0c;同時也保護了數據。對外界而已它的內部細節是隱藏的&#xff0c;暴露給外界的只是它的訪問方法。 繼…

光盤刻錄制作Ubuntu等操作系統的啟動盤

前提條件 軟媒刻錄 空白光盤&#xff08;至少4.7G&#xff09;電腦&#xff08;最好使用外置的光驅&#xff09;系統鏡像&#xff08;ISO格式&#xff09; 具體操作 打開軟媒魔方選擇光盤刻錄按照標紅的進行選擇選擇鏡像->選擇或者拖拽都可以選擇刻錄機->如果使用外部刻…

Java提高篇 —— 抽象類與接口

一、前言 接口和內部類為我們提供了一種將接口與實現分離的更加結構化的方法。 抽象類與接口是java語言中對抽象概念進行定義的兩種機制&#xff0c;正是由于他們的存在才賦予java強大的面向對象的能力。他們兩者之間對抽象概念的支持有很大的相似&#xff0c;甚至可以互換&…

C++ const相關內容學習

const 作用 修飾變量&#xff0c;說明變量不可以被修改修飾指針&#xff0c;分為指向常量的指針&#xff08;pointer to const&#xff09;和自身是常量的指針&#xff08;常量指針&#xff0c;const pointer&#xff09;修飾引用&#xff0c;指向常量的引用&#xff08;refe…

Java提高篇 —— Java淺拷貝和深拷貝

一、前言 我們知道在Java中存在這個接口Cloneable&#xff0c;實現該接口的類都會具備被拷貝的能力&#xff0c;同時拷貝是在內存中進行&#xff0c;在性能方面比我們直接通過new生成對象來的快&#xff0c;特別是在大對象的生成上&#xff0c;使得性能的提升非常明顯。然而我們…

openssl里面AES算法主要函數的參數的介紹

注意事項 使用API的時候&#xff0c;需要特別小心數據長度&#xff0c;一般沒有指定長度的參數&#xff0c;默認都是16&#xff08;AES_BLOCK_SIZE&#xff09;個字節。輸出數據的長度一般都是16字節的倍數&#xff0c;否則會出現數組越界訪問。以下API中&#xff0c;encrypt表…

Java提高篇 —— Java內部類詳解

一、簡介 內部類是一個非常有用的特性但又比較難理解使用的特性。 內部類我們從外面看是非常容易理解的&#xff0c;無非就是在一個類的內部在定義一個類。 public class OuterClass {private String name ;private int age;public String getName() {return name;}public voi…

Ubuntu修改界面的大小

命令 xrandr 就會顯示ubuntu支持的屏幕比例使用命令 xrandr --size 1680x1050 切換屏幕大小