手寫Java LockSupport的示例代碼

前言

在JDK當中給我們提供的各種並發工具當中,比如ReentrantLock等等工具的內部實現,經常會使用到一個工具,這個工具就是LockSupport。LockSupport給我們提供瞭一個非常強大的功能,它是線程阻塞最基本的元語,他可以將一個線程阻塞也可以將一個線程喚醒,因此經常在並發的場景下進行使用。

LockSupport實現原理

在瞭解LockSupport實現原理之前我們先用一個案例來瞭解一下LockSupport的功能!

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
 
public class Demo {
 
  public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread(() -> {
      System.out.println("park 之前");
      LockSupport.park(); // park 函數可以將調用這個方法的線程掛起
      System.out.println("park 之後");
    });
    thread.start();
    TimeUnit.SECONDS.sleep(5);
    System.out.println("主線程休息瞭 5s");
    System.out.println("主線程 unpark thread");
    LockSupport.unpark(thread); // 主線程將線程 thread 喚醒 喚醒之後線程 thread 才可以繼續執行
  }
}

上面的代碼的輸出如下:

park 之前
主線程休息瞭 5s
主線程 unpark thread
park 之後

乍一看上面的LockSupport的park和unpark實現的功能和await和signal實現的功能好像是一樣的,但是其實不然,我們來看下面的代碼:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;
 
public class Demo02 {
  public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread(() -> {
      try {
        TimeUnit.SECONDS.sleep(5);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
      System.out.println("park 之前");
      LockSupport.park(); // 線程 thread 後進行 park 操作 
      System.out.println("park 之後");
    });
    thread.start();
    System.out.println("主線程 unpark thread");
    LockSupport.unpark(thread); // 先進行 unpark 操作
 
  }
}

上面代碼輸出結果如下:

主線程 unpark thread
park 之前
park 之後

在上面的代碼當中主線程會先進行unpark操作,然後線程thread才進行park操作,這種情況下程序也可以正常執行。但是如果是signal的調用在await調用之前的話,程序則不會執行完成,比如下面的代碼:

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
 
public class Demo03 {
 
  private static final ReentrantLock lock = new ReentrantLock();
  private static final Condition condition = lock.newCondition();
 
  public static void thread() throws InterruptedException {
    lock.lock();
 
    try {
      TimeUnit.SECONDS.sleep(5);
      condition.await();
      System.out.println("等待完成");
    }finally {
      lock.unlock();
    }
  }
 
  public static void mainThread() {
    lock.lock();
    try {
      System.out.println("發送信號");
      condition.signal();
    }finally {
      lock.unlock();
      System.out.println("主線程解鎖完成");
    }
  }
 
  public static void main(String[] args) {
    Thread thread = new Thread(() -> {
      try {
        thread();
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    });
    thread.start();
 
    mainThread();
  }
}

上面的代碼輸出如下:

發送信號
主線程解鎖完成

在上面的代碼當中“等待完成“始終是不會被打印出來的,這是因為signal函數的調用在await之前,signal函數隻會對在它之前執行的await函數有效果,對在其後面調用的await是不會產生影響的。

那是什麼原因導致的這個效果呢?

其實JVM在實現LockSupport的時候,內部會給每一個線程維護一個計數器變量_counter,這個變量是表示的含義是“許可證的數量”,隻有當有許可證的時候線程才可以執行,同時許可證最大的數量隻能為1。當調用一次park的時候許可證的數量會減一。當調用一次unpark的時候計數器就會加一,但是計數器的值不能超過1。

當一個線程調用park之後,他就需要等待一個許可證,隻有拿到許可證之後這個線程才能夠繼續執行,或者在park之前已經獲得一個瞭一個許可證,那麼它就不需要阻塞,直接可以執行。

自己動手實現自己的LockSupport

實現原理

在前文當中我們已經介紹瞭locksupport的原理,它主要的內部實現就是通過許可證實現的:

  • 每一個線程能夠獲取的許可證的最大數目就是1。
  • 當調用unpark方法時,線程可以獲取一個許可證,許可證數量的上限是1,如果已經有一個許可證瞭,那麼許可證就不能累加。
  • 當調用park方法的時候,如果調用park方法的線程沒有許可證的話,則需要將這個線程掛起,直到有其他線程調用unpark方法,給這個線程發放一個許可證,線程才能夠繼續執行。但是如果線程已經有瞭一個許可證,那麼線程將不會阻塞可以直接執行。

自己實現LockSupport協議規定

在我們自己實現的Parker當中我們也可以給每個線程一個計數器,記錄線程的許可證的數目,當許可證的數目大於等於0的時候,線程可以執行,反之線程需要被阻塞,協議具體規則如下:

  • 初始線程的許可證的數目為0。
  • 如果我們在調用park的時候,計數器的值等於1,計數器的值變為0,則線程可以繼續執行。
  • 如果我們在調用park的時候,計數器的值等於0,則線程不可以繼續執行,需要將線程掛起,且將計數器的值設置為-1。
  • 如果我們在調用unpark的時候,被unpark的線程的計數器的值等於0,則需要將計數器的值變為1。
  • 如果我們在調用unpark的時候,被unpark的線程的計數器的值等於1,則不需要改變計數器的值,因為計數器的最大值就是1。
  • 我們在調用unpark的時候,如果計數器的值等於-1,說明線程已經被掛起瞭,則需要將線程喚醒,同時需要將計數器的值設置為0。

工具

因為涉及線程的阻塞和喚醒,我們可以使用可重入鎖ReentrantLock和條件變量Condition,因此需要熟悉這兩個工具的使用。

ReentrantLock 主要用於加鎖和開鎖,用於保護臨界區。

Condition.awat 方法用於將線程阻塞。

Condition.signal 方法用於將線程喚醒。

因為我們在unpark方法當中需要傳入具體的線程,將這個線程發放許可證,同時喚醒這個線程,因為是需要針對特定的線程進行喚醒,而condition喚醒的線程是不確定的,因此我們需要為每一個線程維護一個計數器和條件變量,這樣每個條件變量隻與一個線程相關,喚醒的肯定就是一個特定的線程。我們可以使用HashMap進行實現,鍵為線程,值為計數器或者條件變量。

具體實現

因此綜合上面的分析我們的類變量如下:

private final ReentrantLock lock; // 用於保護臨界去
private final HashMap<Thread, Integer> permits; // 許可證的數量
private final HashMap<Thread, Condition> conditions; // 用於喚醒和阻塞線程的條件變量

構造函數主要對變量進行賦值:

public Parker() {
  lock = new ReentrantLock();
  permits = new HashMap<>();
  conditions = new HashMap<>();
}

park方法

public void park() {
  Thread t = Thread.currentThread(); // 首先得到當前正在執行的線程
  if (conditions.get(t) == null) { // 如果還沒有線程對應的condition的話就進行創建
    conditions.put(t, lock.newCondition());
  }
  lock.lock();
  try {
    // 如果許可證變量還沒有創建 或者許可證等於0 說明沒有許可證瞭 線程需要被掛起
    if (permits.get(t) == null || permits.get(t) == 0) {
      permits.put(t, -1); // 同時許可證的數目應該設置為-1
      conditions.get(t).await();
    }else if (permits.get(t) > 0) {
      permits.put(t, 0); // 如果許可證的數目大於0 也就是為1 說明線程已經有瞭許可證因此可以直接被放行 但是需要消耗一個許可證
    }
  } catch (InterruptedException e) {
    e.printStackTrace();
  } finally {
    lock.unlock();
  }
}

unpark方法

public void unpark(Thread thread) {
  Thread t = thread; // 給線程 thread 發放一個許可證
  lock.lock();
  try {
    if (permits.get(t) == null) // 如果還沒有創建許可證變量 說明線程當前的許可證數量等於初始數量也就是0 因此方法許可證之後 許可證的數量為 1
      permits.put(t, 1);
    else if (permits.get(t) == -1) { // 如果許可證數量為-1,則說明肯定線程 thread 調用瞭park方法,而且線程 thread已經被掛起瞭 因此在 unpark 函數當中不急需要將許可證數量這是為0 同時還需要將線程喚醒
      permits.put(t, 0);
      conditions.get(t).signal();
    }else if (permits.get(t) == 0) { // 如果許可證數量為0 說明線程正在執行 因此許可證數量加一
      permits.put(t, 1);
    } // 除此之外就是許可證為1的情況瞭 在這種情況下是不需要進行操作的 因為許可證最大的數量就是1
  }finally {
    lock.unlock();
  }
}

完整代碼

import java.util.HashMap;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
 
public class Parker {
 
  private final ReentrantLock lock;
  private final HashMap<Thread, Integer> permits;
  private final HashMap<Thread, Condition> conditions;
 
  public Parker() {
    lock = new ReentrantLock();
    permits = new HashMap<>();
    conditions = new HashMap<>();
  }
 
  public void park() {
    Thread t = Thread.currentThread();
    if (conditions.get(t) == null) {
      conditions.put(t, lock.newCondition());
    }
    lock.lock();
    try {
      if (permits.get(t) == null || permits.get(t) == 0) {
        permits.put(t, -1);
        conditions.get(t).await();
      }else if (permits.get(t) > 0) {
        permits.put(t, 0);
      }
    } catch (InterruptedException e) {
      e.printStackTrace();
    } finally {
      lock.unlock();
    }
  }
 
  public void unpark(Thread thread) {
    Thread t = thread;
    lock.lock();
    try {
      if (permits.get(t) == null)
        permits.put(t, 1);
      else if (permits.get(t) == -1) {
        permits.put(t, 0);
        conditions.get(t).signal();
      }else if (permits.get(t) == 0) {
        permits.put(t, 1);
      }
    }finally {
      lock.unlock();
    }
  }
}

JVM實現一瞥

其實在JVM底層對於park和unpark的實現也是基於鎖和條件變量的,隻不過是用更加底層的操作系統和libc(linux操作系統)提供的API進行實現的。雖然API不一樣,但是原理是相仿的,思想也相似。

比如下面的就是JVM實現的unpark方法:

void Parker::unpark() {
  int s, status;
  // 進行加鎖操作 相當於 可重入鎖的 lock.lock()
  status = pthread_mutex_lock(_mutex);
  assert (status == 0, "invariant");
  s = _counter;
  _counter = 1;
  if (s < 1) {
    // 如果許可證小於 1 進行下面的操作
    if (WorkAroundNPTLTimedWaitHang) {
      // 這行代碼相當於 condition.signal() 喚醒線程
      status = pthread_cond_signal (_cond);
      assert (status == 0, "invariant");
      // 解鎖操作 相當於可重入鎖的 lock.unlock()
      status = pthread_mutex_unlock(_mutex);
      assert (status == 0, "invariant");
    } else {
      status = pthread_mutex_unlock(_mutex);
      assert (status == 0, "invariant");
      status = pthread_cond_signal (_cond);
      assert (status == 0, "invariant");
    }
  } else {
    // 如果有許可證 也就是 s == 1 那麼不許要將線程掛起
    // 解鎖操作 相當於可重入鎖的 lock.unlock()
    pthread_mutex_unlock(_mutex);
    assert (status == 0, "invariant");
  }
}

JVM實現的park方法,如果沒有許可證也是會將線程掛起的:

總結

在本篇文章當中主要介紹啦lock support的用法以及它的大致原理,以及介紹啦我們自己該如何實現類似lock support的功能,並且定義瞭我們自己實現lock support的大致協議,整個過程還是比較清晰的,我們隻是實現瞭lock support當中兩個核心方法,其他的方法其實也類似,原理差不多,在這裡咱就實現一個乞丐版的lock support的吧!!!

使用鎖和條件變量進行線程的阻塞和喚醒。

使用Thread.currentThread()方法得到當前正在執行的線程。

使用HashMap去存儲線程和許可證以及條件變量的關系。

以上就是手寫Java LockSupport的示例代碼的詳細內容,更多關於Java LockSupport的資料請關註WalkonNet其它相關文章!

推薦閱讀: