利用單例模式解決全局訪問問題


      在面向對象編程中,我們無時無刻都可能在產生對象,因為我們的代碼需要對象,但值得注意的是,我們有時候也有可能是在無謂的產生對象,更加可怕的是,這些累贅的對象會造成難以排查的BUG,尤其是在多線程編程中。

      所以,合理的產生對象也是一個學問。

     有些對象我們只需要一個,像是線程池,緩沖等,這類對象只能有一個實例,一旦產生多個實例就會出現問題。所以,我們必須找到一種方法來確保我們的代碼中只有一個實例。

     首先我們想到的第一個解決方法就是聲明一個全局變量,然后將對象賦值給該全局變量,但是這意味着我們必須在程序一開始的時候就要創建好該對象,但我們應該是在需要的時候才創建對象,而且如果這個對象本身非常耗費資源的話,這就是一種浪費。

     提到創建對象,這里有一點必須要提到:

static BluetoothSocket socket = null;

     因為java鼓勵人們在聲明對象的時候賦予對象初始值以免出現問題,但對象這時候並沒有被創建!真正的創建對象應該是通過new和反射機制來產生。
     使用全局變量的時候,我們必須確保整個程序中只有一份,因為使用全局變量的目的就是為了共享資源,而共享資源必須到達兩個條件:不變和共享。不變,指的是這個資源在整個程序中必須只有一份,而共享是指它的變化必須對共享它的所有對象是可見的,否則,所有對象都有自己的一份,何來共享呢?

     因此,我們必須確保一件事:一個類只有一個實例,其他類無法自行產生它的實例。

     使用單例模式就能確保這點。

     單例模式的意圖是:確保一個類只有一個實例,並且提供一個全局訪問點。它到底是怎么做到這點的呢?

     我們知道,一般類是通過構造器來產生的,而構造器一般都是public,也就是可訪問的,但如果我們將構造器設為private,那么也就能阻止其他類產生該對象的實例,但問題也就來了:私有的構造器只有該類才能訪問,但是我們無法產生該類的實例,又如何能得到該類的實例呢?

     這時我們就需要在該類中聲明一個自己本身的靜態實例,然后通過靜態方法返回。我們知道,靜態實例在程序中只有一份,所以這也就能確保該實例在程序中只有一份,並且因為構造器是私有的,其他類也就無法產生實例。

      下面是使用單例模式的經典用法:

private static BluetoothSocket bluetoothSocket;

private BluetoothSocket(){}

public static BluetoothSicket getBluetoothSocket(){
     if(blueSocket == null){
           bluetoothSocket = new BluetoothSokcet();
     }
     return bluetoothSocket;
}

      因為getBluetoothSocket()是一個靜態方法,因為我們不需要通過創建實例來調用該方法,而且這里還使用了創建對象時經常使用到的方法:延遲實例化,又叫滯后初始化,利用這點,我們可以先判斷程序中是否已經創建了實例,如果沒有才創建實例,這樣就能確保程序中永遠只有一份實例了。使用延遲實例化的最大好處就是我們可以在需要的時候才創建對象。

      單例模式是為了消除程序員無謂的創建全局變量,尤其是新手,像是我一開始編程的時候,就喜歡創建全局變量,因為不用考慮命名空間這些東西真心舒服,因為那時候我還在學習C和C++,即使java表面上沒有命名空間的說法,但其實內部的機制也是基於命名空間,只不過是用包導入機制確保我們不需要為這個問題煩惱而已。

      無論是哪種程序語言,都不鼓勵使用全局變量,那意味着編程結構不好。

      如果是一般的編碼,單例模式好像有點大材小用了,但如果是多線程編程這種讓人糾結的東西,單例模式在一定程度上給予了線程安全。

      同步這個話題是我們學習java跳不過的,而且也是一個非常重要的難點,要想寫出一個線程安全的代碼,是需要我們不斷努力的。

      就算是單例模式,我們也無法確保兩個線程不會同時創建實例,最糟糕的的情況就是多個線程同時調用靜態方法同時產生實例,這在多線程中是非常常見的現象。因此,我們可以在靜態方法前添加synchronized關鍵字來迫使每個線程在進入這個方法前,要先等候別的線程離開該方法,以確保不會有多個線程同時調用該方法。

      但問題還是來了:只有第一次執行該方法的時候我們才真正需要同步,因為靜態方法在第一次被調用后就能確保不會再產生實例了。synchronized關鍵字這時就是個累贅了,它只在第一次時有用,以后每次調用該方法的時候都要為此付出無謂的線程消耗。

      所以,同步一個方法並不是一個好的做法,它會讓我們的程序存在一個無限等待的后台,導致我們的程序效率非常低下。

      我們只要稍微改動一下代碼就行:

private static BluetoothSocket bluetoothSocket = new BluetoothSocket();

private BluetoothSocket(){}

public static BluetoothSocket getBluetooth(){
    return bluetoothSocket;
}

      為什么這段代碼就能保證線程安全呢?明明它就只是廢棄了延遲實例化而已。因為這樣JVM能夠確保在任何線程調用靜態方法前就已經創建好該實例了。

      我們還可以利用線程的其他知識來完成這個任務:

private volatile static BluetoothSocket bluetoothSocket;

private BluetoothSocket(){}

public static BluetoothSicket getBluetoothSocket(){
     if(blueSocket == null){
           synchronized(BluetoothSocket.class){
               if(blueSocket == null){
                  bluetoothSocket = new BluetoothSokcet();
               }
           }
     }
     return bluetoothSocket;
}

       這就是多重檢查加鎖的做法。我們首先檢查實例是否已經創建,如果尚未創建,才進行同步。
       volatile關鍵字能夠確保實例被創建時,所有線程都能正確處理該變量。什么叫正確的處理,就是我們利用volatile同步了該實例,在該實例發生變化的時候,其他共享線程都能知道從而進行相應的處理。多重檢查加鎖需要利用到對象鎖,每個java對象身上都有一個鎖,當一個線程獲取到該鎖后,就會防止其他線程試圖訪問該對象,除非該線程釋放了這個對象的鎖。volatile一般都是要和對象鎖一起搭配才能發揮真正的作用,一個控制住了變化,另一個則是控制住了訪問。

       單例模式之所以能夠確保只有一個實例,也是建立在只有一個類加載器的前提下,如果有兩個以上的類加載器,就有可能產生多個單例並存的奇怪現象。所以,這時我們必須指定類加載器。但一般的程序都只有一個類加載器,但涉及到多線程的話,這個可就不知道了。

       總結一下,單例模式的最大作用就是將一個對象的職責全部集中在一個實例上,這樣就能避免無謂的浪費,但單例模式也並不是一個可以隨便亂用,它就和全局變量的使用是一樣的,將所有職責交給一個實例,這對實例本身就是一個不公平,尤其是職責非常重大的時候。


      

      
    


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM