1. 程式人生 > >《Java原始碼解析》之NIO的Selector機制(Part1:Selector.open())

《Java原始碼解析》之NIO的Selector機制(Part1:Selector.open())

Selector機制之Selector.open()函式的解析

在NIO中我們一般都是Channel與Selector配合使用的,一般情況下使用的方法如下:

//開啟Selector來處理channel
Selector selector = Selector.open();
//將channel註冊到selector中,並將channel設定成等待新的連線
serverChannel.register(selector,SelectionKey.OP_ACCEPT);
//等待處理新的事件;一直阻塞直到下一個事件到來才喚醒.此方法執行處於阻塞模式的選擇操作。僅在至少選擇一個通道、呼叫此選擇器的 wakeup 方法,或者當前的執行緒已中斷(以先到者為準)後此方法才返回。
selector.select();

這篇部落格我們主要從Selector.open()函式開始,一步步分析Selector機制。

  1. 首先我們進入Selector.open();函式,在JDK原始碼中定義如下:
public static Selector open() throws IOException {
        return SelectorProvider.provider().openSelector();
    }

檢視這個函式的doc文件註釋:
The new selector is created by invoking the openSelector method of the system-wide default SelectorProvider object.
意思是:通過呼叫系統預設的SelectorProvider(這裡不同的系統會有不同的SelectorProvider實現類)的openSelector()方法來建立新的selector。

這裡我們首先分析一下:
SelectorProvider.provider();
進入SelectorProvider這個類的靜態方法provider(),JDK的原始碼如下:

public static SelectorProvider provider() {
        /**
          *加鎖,鎖的定義是:private static final Object lock = new Object();
          */
        synchronized (lock) {
            /**
              *provider的定義是:private static SelectorProvider provider = null;
              */
if (provider != null) return provider; return AccessController.doPrivileged( new PrivilegedAction<SelectorProvider>() { public SelectorProvider run() { if (loadProviderFromProperty()) return provider; if (loadProviderAsService()) return provider; provider = sun.nio.ch.DefaultSelectorProvider.create(); return provider; } }); } }

還是先來閱讀一下Javadoc中關於這個函式的功能描述:通過Java虛擬機器的呼叫返回系統預設的selector provider。
注意:我們發現同步程式碼段中首先的就是一個判斷:

if (provider != null)
    return provider;

這個地方判斷provider在當前程序是否已經被例項化過了,如果已經被例項化過了,那麼就直接返回當前provider,不再執行後面的程式碼;否者就執行後面的程式碼例項化provider。這裡就是一個最簡單的 單例模式 的應用。

繼續分析後面的程式碼:後面是一個AccessController.doPrivileged()這個貌似是與許可權相關的,不是本文的重點,暫時不去分析這個。我們分析後面的run()方法裡面的內容:

if (loadProviderFromProperty())
   return provider;
if (loadProviderAsService())
   return provider;
provider = sun.nio.ch.DefaultSelectorProvider.create();
return provider;

loadProviderFromProperty()這個函式判斷如果系統屬性java.nio.channels.spi.SelectorProvider 已經被定義了,則該屬性名看作具體提供者類的完全限定名。載入並例項化該類;如果此程序失敗,則丟擲未指定的錯誤。
loadProviderAsService()這個函式判斷:如果在對系統類載入器可見的 jar 檔案中安裝了提供者類,並且該 jar 檔案包含資源目錄 META-INF/services 中名為 java.nio.channels.spi.SelectorProvider 的提供者配置檔案,則採用在該檔案中指定的第一個類名稱。載入並例項化該類;如果此程序失敗,則丟擲未指定的錯誤。
最後,如果未通過上述的方式制定任何provider,則例項化系統預設的provider並返回該結果(一般情況下,都是這種情況。)

這個地方需要注意的是:這裡系統預設的provider在不同系統上是不一樣的,下面用一個表格來表示:

系統 provider 備註
MacOSX KQueueSelectorProvider
Linux
Windows

進入sun.nio.ch.DefaultSelectorProvider.create();
這裡系統會根據不同的作業系統返回不同的provider;我們來看一下系統預設的provider;由於我用的是Mac-OSX 的系統進入之後原始碼如下:

public static SelectorProvider create() {
    return new KQueueSelectorProvider();
}

所以OSX預設的provider是KQueueSelectorProvider。繼續進入KQueueSelectorProvider的建構函式,原始碼如下:

public KQueueSelectorProvider() {
}

這是一個空的建構函式,所以SelectorProvider.provider();
最後返回的就是KQueueSelectorProvider;下面給個示圖顯示類的繼承關係:
這裡寫圖片描述

我們繼續看SelectorProvider.provider().openSelector();
後面呼叫的也就是KQueueSelectorProvider.openSelector();原始碼如下:

public AbstractSelector openSelector() throws IOException {
    return new KQueueSelectorImpl(this);
}

所以最後返回的就是KQueueSelectorImpl這個實現類的例項。我們看一下這個類的繼承關係圖:
這裡寫圖片描述

下面就是分析KQueueSelectorImpl的建構函式,來窺探一下selector的執行原理:

//建構函式
KQueueSelectorImpl(SelectorProvider var1) {
    //呼叫父類的建構函式,傳入的是一個SelectorProvider,在Mac上是KQueueSelectorProvider
    super(var1);
    long var2 = IOUtil.makePipe(false);//native方法
    this.fd0 = (int)(var2 >>> 32);//高32位存放的是Pipe管道的讀端的檔案描述符
    this.fd1 = (int)var2;//低32位存放的是Pipe管道的寫端的檔案描述符
    this.kqueueWrapper = new KQueueArrayWrapper();
    this.kqueueWrapper.initInterrupt(this.fd0, this.fd1);
    this.fdMap = new HashMap();
    this.totalChannels = 1;
}

IOUtil.makePipe(false); 是一個static native方法,所以我們沒辦法檢視原始碼。但是我們可以知道該函式返回了一個非堵塞的管道(pipe),底層是通過Linux的pipe系統呼叫實現的;建立了一個管道pipe並返回了一個64為的long型整數,該數的高32位存放了該管道讀端的檔案描述符,低32位存放了該pipe的寫端的檔案描述符。

下面又建立了一個KQueueArrayWrapper物件,我們把重點放在下面的this.kqueueWrapper.initInterrupt(this.fd0, this.fd1);函式,這裡把管道的讀寫兩端的檔案描述符作為引數傳入,我們先來看看原始碼:

void initInterrupt(int var1, int var2) {
//private int outgoingInterruptFD;
//private int incomingInterruptFD;
    this.outgoingInterruptFD = var2;//寫端
    this.incomingInterruptFD = var1;//讀端
    this.register0(this.kq, var1, 1, 0);
}

其中的register0(this.kq, var1, 1, 0);又是一個native方法。initInterrup方法,從該方法初步判斷,管道應該和中斷有關係;

最後定義了一個map型別的變數fdToKey,將channel的檔案描述符和SelectionKey建立對映關係;

最後總結一下Selector.open()幹了啥:
主要完成建立Pipe,並把pipe的讀寫檔案描述符放入pollArray中,這個pollArray是Selector的樞紐。Linux下則是直接使用系統的pipe。
下面附圖展示Selector工作原理,以Windows下為例:
這裡寫圖片描述