Java併發系列—工具類:Semaphore

牛覓發表於2018-05-15

原文地址:cnblogs.com/liuyun1995/p/8474026.html 作者:勞夫子

Semaphore(訊號量)用來控制同時訪問特定資源的執行緒數量,它通過協調各個執行緒,以保證合理的使用公共資源。

Semaphore提供了一個許可證的概念,可以把這個許可證看作公共汽車車票,只有成功獲取車票的人才能夠上車,並且車票是有一定數量的,不可能毫無限制的發下去,這樣就會導致公交車超載。所以當車票發完的時候(公交車以滿載),其他人就只能等下一趟車了。如果中途有人下車,那麼他的位置將會空閒出來,因此如果這時其他人想要上車的話就又可以獲得車票了。

建構函式

Semaphore類提供了2種建構函式,分別如下:

public Semaphore(int permits) {
    sync = new NonfairSync(permits);
}

public Semaphore(int permits, boolean fair) {
    sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
複製程式碼

這兩個構造方法,都必須提供許可的數量,第二個構造方法可以指定是公平模式還是非公平模式,預設非公平模式。

在ReentrantLock中公平鎖和非公平鎖獲取鎖機制的差別:對於公平鎖而言,如果當前執行緒不在CLH佇列的頭部,則需要排隊等候,而非公平鎖則不同,它無論當前執行緒處於CLH佇列的何處都會直接獲取鎖。所以公平訊號量和非公平訊號量的區別也一樣。

獲取許可證

Semaphore類提供了4種獲取許可證的方法,分別如下:

//獲取一個許可證(響應中斷),在沒有可用的許可證時當前執行緒被阻塞。
public void acquire() throws InterruptedException {
   sync.acquireSharedInterruptibly(1);
}

//獲取一個許可證(不響應中斷)
public void acquireUninterruptibly() {
   sync.acquireShared(1);
}

//嘗試獲取許可證(非公平獲取),立即返回結果(非阻塞)。
public boolean tryAcquire() {
   return sync.nonfairTryAcquireShared(1) >= 0;
}

//嘗試獲取許可證(定時獲取)
public boolean tryAcquire(long timeout, TimeUnit unit) throws InterruptedException {
   return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
複製程式碼

下面我們一起看看,acquire()是如何獲取許可證的? 其原始碼如下:

 public void acquire() throws InterruptedException {
    sync.acquireSharedInterruptibly(1);
}

public final void acquireSharedInterruptibly(int arg)
        throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    if (tryAcquireShared(arg) < 0)
        doAcquireSharedInterruptibly(arg);
}
複製程式碼

acquireSharedInterruptibly方法首先就是去呼叫tryAcquireShared方法去嘗試獲取,tryAcquireShared在AQS裡面是抽象方法,FairSync和NonfairSync這兩個派生類實現了該方法的邏輯。FairSync實現的是公平獲取的邏輯,而NonfairSync實現的非公平獲取的邏輯。

abstract static class Sync extends AbstractQueuedSynchronizer {
    private static final long serialVersionUID = 1192457210091910933L;

    Sync(int permits) {
        setState(permits);
    }

    final int getPermits() {
        return getState();
    }

    // 非公平方式嘗試獲取
    final int nonfairTryAcquireShared(int acquires) {
        for (;;) {
             // 獲取可用許可證
            int available = getState();
            // 獲取剩餘許可證
            int remaining = available - acquires;
            // 1.如果remaining小於0則直接返回remaining
            // 2.如果remaining大於0則先更新同步狀態再返回remaining
            if (remaining < 0 ||
                compareAndSetState(available, remaining))
                return remaining;
        }
    }

    protected final boolean tryReleaseShared(int releases) {
        for (;;) {
            int current = getState();
            int next = current + releases;
            if (next < current) // overflow
                throw new Error("Maximum permit count exceeded");
            if (compareAndSetState(current, next))
                return true;
        }
    }

    final void reducePermits(int reductions) {
        for (;;) {
            int current = getState();
            int next = current - reductions;
            if (next > current) // underflow
                throw new Error("Permit count underflow");
            if (compareAndSetState(current, next))
                return;
        }
    }

    final int drainPermits() {
        for (;;) {
            int current = getState();
            if (current == 0 || compareAndSetState(current, 0))
                return current;
        }
    }
}

// 非公平同步器
static final class NonfairSync extends Sync {
    private static final long serialVersionUID = -2694183684443567898L;

    NonfairSync(int permits) {
        super(permits);
    }

    // 嘗試獲取許可證
    protected int tryAcquireShared(int acquires) {
        return nonfairTryAcquireShared(acquires);
    }
}

// 公平同步器
static final class FairSync extends Sync {
    private static final long serialVersionUID = 2014338818796000944L;

    FairSync(int permits) {
        super(permits);
    }

    // 嘗試獲取許可證
    protected int tryAcquireShared(int acquires) {
        for (;;) {
            // 判斷同步佇列前面存在排隊
            if (hasQueuedPredecessors())
                return -1;
            int available = getState();
            int remaining = available - acquires;
            if (remaining < 0 ||
                compareAndSetState(available, remaining))
                return remaining;
        }
    }
}
複製程式碼
  • 非公平獲取鎖的邏輯是先取出當前同步狀態(同步狀態表示許可證個數),將當前同步狀態減去參入的引數,如果結果不小於0的話證明還有可用的許可證,那麼就直接使用CAS操作更新同步狀態的值,最後不管結果是否小於0都會返回該結果值。

  • 公平獲取鎖的邏輯僅僅是在此之前先去呼叫hasQueuedPredecessors方法判斷同步佇列是否存在排隊,如果有的話就直接返回-1表示獲取失敗,否則繼續執行和非公平獲取一樣的步驟。

釋放許可證

Semaphore類提供了2種釋放許可證方法,分別如下:

public void release() {
    sync.releaseShared(1);
}

public void release(int permits) {
    if (permits < 0) throw new IllegalArgumentException();
    sync.releaseShared(permits);
}
複製程式碼

呼叫release方法是釋放一個許可證,它的操作很簡單,就呼叫了AQS的releaseShared方法。

//釋放鎖的操作(共享模式)
public final boolean releaseShared(int arg) {
   //1.嘗試去釋放鎖
   if (tryReleaseShared(arg)) {
       //2.如果釋放成功就喚醒其他執行緒
       doReleaseShared();
       return true;
   }
   return false;
}
複製程式碼

AQS的releaseShared方法首先呼叫tryReleaseShared方法嘗試釋放鎖,其實現邏輯在子類Sync裡面。

  // 嘗試釋放操作
  protected final boolean tryReleaseShared(int releases) {
    for (;;) {
        // 獲取當前同步狀態
        int current = getState();
        // 將當前同步狀態加上傳入的引數
        int next = current + releases;
        if (next < current) // overflow
            throw new Error("Maximum permit count exceeded");
        // 以CAS方式更新同步狀態的值, 更新成功則返回true, 否則繼續迴圈
        if (compareAndSetState(current, next))
            return true;
    }
}
複製程式碼

tryReleaseShared方法裡面採用for迴圈進行自旋,首先獲取同步狀態,將同步狀態加上傳入的引數,然後以CAS方式更新同步狀態,更新成功就返回true並跳出方法,否則就繼續迴圈直到成功為止。

動手寫個連線池

下面我們就來利用Semaphore實現一個簡單的資料庫連線池,通過這個例子希望讀者們能更加深入的掌握Semaphore的運用。

public class ConnectPool {
   
   //連線池大小
   private int size;
   //資料庫連線集合
   private Connect[] connects;
   //連線狀態標誌
   private boolean[] connectFlag;
   //剩餘可用連線數
   private volatile int available;
   //訊號量
   private Semaphore semaphore;
   
   //構造器
   public ConnectPool(int size) {  
       this.size = size;
       this.available = size;
       semaphore = new Semaphore(size, true);
       connects = new Connect[size];
       connectFlag = new boolean[size];
       initConnects();
   }
   
   //初始化連線
   private void initConnects() {
       //生成指定數量的資料庫連線
       for(int i = 0; i < this.size; i++) {
           connects[i] = new Connect();
       }
   }
   
   //獲取資料庫連線
   private synchronized Connect getConnect(){  
       for(int i = 0; i < connectFlag.length; i++) {
           //遍歷集合找到未使用的連線
           if(!connectFlag[i]) {
               //將連線設定為使用中
               connectFlag[i] = true;
               //可用連線數減1
               available--;
               System.out.println("【"+Thread.currentThread().getName()+"】以獲取連線      剩餘連線數:" + available);
               //返回連線引用
               return connects[i];
           }
       }
       return null;
   }
   
   //獲取一個連線
   public Connect openConnect() throws InterruptedException {
       //獲取許可證
       semaphore.acquire();
       //獲取資料庫連線
       return getConnect();
   }
   
   //釋放一個連線
   public synchronized void release(Connect connect) {  
       for(int i = 0; i < this.size; i++) {
           if(connect == connects[i]){
               //將連線設定為未使用
               connectFlag[i] = false;
               //可用連線數加1
               available++;
               System.out.println("【"+Thread.currentThread().getName()+"】以釋放連線      剩餘連線數:" + available);
               //釋放許可證
               semaphore.release();
           }
       }
   }
   
   //剩餘可用連線數
   public int available() {
       return available;
   }
   
}
複製程式碼

測試程式碼:

public class TestThread extends Thread {
   
   private static ConnectPool pool = new ConnectPool(3);
   
   @Override
   public void run() {
       try {
           Connect connect = pool.openConnect();
           Thread.sleep(100);  //休息一下
           pool.release(connect);
       } catch (InterruptedException e) {
           e.printStackTrace();
       }
   }
   
   public static void main(String[] args) {
       for(int i = 0; i < 10; i++) {
           new TestThread().start();
       }
   }

}
複製程式碼

使用一個陣列來存放資料庫連線的引用,在初始化連線池的時候會呼叫initConnects方法建立指定數量的資料庫連線,並將它們的引用存放到陣列中,此外還有一個相同大小的陣列來記錄連線是否可用。

每當外部執行緒請求獲取一個連線時,首先呼叫semaphore.acquire()方法獲取一個許可證,然後將連線狀態設定為使用中,最後返回該連線的引用。許可證的數量由構造時傳入的引數決定,每呼叫一次semaphore.acquire()方法許可證數量減1,當數量減為0時說明已經沒有連線可以使用了,這時如果其他執行緒再來獲取就會被阻塞。每當執行緒釋放一個連線的時候會呼叫semaphore.release()將許可證釋放,此時許可證的總量又會增加,代表可用的連線數增加了,那麼之前被阻塞的執行緒將會醒來繼續獲取連線,這時再次獲取就能夠成功獲取連線了。

歡迎留言補充,共同交流。個人微信公眾號求關注

Java併發系列—工具類:Semaphore

相關文章