有些東西學完就忘,怎麼辦呢?
還能咋辦,忘了再學唄!
前言
我們爲什麼需要單例模式呢?每次都new一個對象不行嗎?
根據我這個快要畢業的、沒有開發經驗的菜鳥來看,內存是有數的,並不是無限的,所以單例模式是爲了儘可能的節約資源,只有一個對象和頻繁的創建對象,孰優孰略想想就知道。
餓漢式
概念就是很神奇,一眼下去是個懵逼的詞,等你弄懂了又發現這個詞確實還挺形象準確。
怎麼實現單例?肯定不能隨便讓你new一個對象,把構造器私有化。那怎麼獲取對象呢?自然是暴露出來一個方法能夠獲取這個對象。首先我們來看這個餓漢式的實現方式。什麼是餓漢式呢?就是當類加載完畢,對象就創建好了,不管你用不用,先給你創建上。我們看代碼:
public class TaskManageHungry {
private static TaskManageHungry taskManageHungry = new TaskManageHungry();
//構造器私有
private TaskManageHungry() {
System.out.println("執行構造器===>");
}
//獲取這個對象的方法
public static TaskManageHungry getInstance(){
return taskManageHungry;
}
//測試這個單例模式
public static void main(String[] args) {
TaskManageHungry taskManageHungry1 = TaskManageHungry.getInstance();
TaskManageHungry taskManageHungry2 = TaskManageHungry.getInstance();
TaskManageHungry taskManageHungry3 = TaskManageHungry.getInstance();
System.out.println(taskManageHungry1);
System.out.println(taskManageHungry2);
System.out.println(taskManageHungry3);
}
}
這樣我們就實現了這個餓漢式的單例模式,每次都是同一個對象,我們可以運行看看。
但是呢,這樣雖然實現了單例模式,可它還是有點問題。怎麼說呢,我們先回歸一下單例模式的初心——節約資源。那假如這個對象我用的次數並不多,而且系統啓動後很長時間我才能用得上這個對象,那它直接創建出來,很長時間不被使用,被它佔用的那點內存不就浪費了嗎?所以這就又引出了懶漢式單例。
懶漢式
什麼是懶漢式呢?就是我並不急着創建這個對象,只有當它第一次被調用的時候再創建這個對象。我們還是直接看代碼
public class TaskManageLazy {
private TaskManageLazy(){
System.out.println("懶漢式構造器執行");
}
private static TaskManageLazy taskManageLazy;
//獲取實例
public static TaskManageLazy getInstance(){
if (taskManageLazy == null) {
taskManageLazy = new TaskManageLazy();
}
return taskManageLazy;
}
}
我們在獲取對象的方法里加一個判斷,這樣就實現了懶漢式單例。沒人用的時候,它就是個null,有人調用了,纔會創建對象。
但是這樣又出現了新的問題,比如我有十個線程,同時調用這個對象,十個線程都是第一次調用,那豈不是就又創建了好幾個對象,單例模式就被破壞了,我們來看看這個多線程情況下會出現什麼樣的結果。
public static void main(String[] args) {
//測試多線程下
for (int i = 0; i < 10; i++) {
new Thread(() -> System.out.println(TaskManageLazy.getInstance())).start();
}
}
開十個線程來調用,我們來看運行結果。
這說明我們的懶漢式單例還是不夠完善,那怎麼才能在多線程情況下也能實現單例模式呢?當然是上鎖。
public class TaskManageLazy {
private TaskManageLazy(){
System.out.println("懶漢式構造器執行");
}
private static TaskManageLazy taskManageLazy;
//獲取實例
public static TaskManageLazy getInstance(){
synchronized (TaskManageLazy.class) {
if (taskManageLazy == null) {
taskManageLazy = new TaskManageLazy();
}
}
return taskManageLazy;
}
}
這樣鎖起來確實可以保證線程安全,但是我們想一下,只有第一次執行的時候才需要加鎖來創建對象。如果要是這樣寫,那我每次獲取實例都要鎖起來,不管有沒有對象都要鎖起來,假如我有一百個線程要獲取這個對象,那這一百個線程豈不是要排隊等待着一個個獲取。所以,我們還要把它改造一下,沒有對象的時候加鎖,有對象了就直接返回。
public class TaskManageLazy {
private TaskManageLazy(){
System.out.println("懶漢式構造器執行");
}
private static TaskManageLazy taskManageLazy;
//獲取實例
public static TaskManageLazy getInstance(){
if (taskManageLazy == null){
synchronized (TaskManageLazy.class) {
if (taskManageLazy == null) {
taskManageLazy = new TaskManageLazy();
}
}
}
return taskManageLazy;
}
}
這種方式就叫做雙重檢查鎖。檢查了兩次null可不就是雙重嘛。
瞭解到這裏其實也還行,不過我們能不能更深入一點呢?這樣就真的安全了嗎?假如有人用反射破壞了這個單例怎麼辦呢?有沒有一種辦法一勞永逸呢?
這些事情,就以後再聊吧。