DesignPattern系列__10單例模式

来源:https://www.cnblogs.com/JackHou/archive/2019/08/07/11317284.html
-Advertisement-
Play Games

單例模式介紹 單例模式,是為了確保在整個軟體體統中,某個類對象只有一個實例,並且該類通常會提供一個對外獲取該實例的public方法(靜態方法)。 比如日誌、資料庫連接池等對象,通常需要且只需要一個實例對象,這就會使用單例模式。 單例模式的八種模式 餓漢式(靜態常量) 餓漢式(靜態代碼塊) 懶漢式(線 ...


單例模式介紹

單例模式,是為了確保在整個軟體體統中,某個類對象只有一個實例,並且該類通常會提供一個對外獲取該實例的public方法(靜態方法)。
比如日誌、資料庫連接池等對象,通常需要且只需要一個實例對象,這就會使用單例模式。

單例模式的八種模式

  • 餓漢式(靜態常量)
  • 餓漢式(靜態代碼塊)
  • 懶漢式(線程不安全)
  • 懶漢式(同步方法)
  • 懶漢式(同步代碼塊)
  • 懶漢式(雙重檢查)
  • 靜態內部類
  • 枚舉

下麵依次來說明一下:

餓漢式(靜態常量)

通常,我們創建一個對象的方式就是new,但是,當我們考慮只創建一個實例的時候,就應該禁止外部來通過new的方式進行創建。同時,由於無法使用new,你應該考慮提供一個獲取單例對象的方式給別人。

思路

1.將構造器私有化(防止外部new,但是對反射還是有局限)
2.類的內部創建對象
3.對外提供一個獲取實例靜態的public方法

代碼實現:

public class Singleton1 {
    public static void main(String[] args) {
        HungrySingleton hungrySingleton = HungrySingleton.getInstance();
        HungrySingleton hungrySingleton1 = HungrySingleton.getInstance();
        System.out.println(hungrySingleton == hungrySingleton1);
    }
}

class HungrySingleton {
    //1.私有化構造器
    private HungrySingleton() {
    }

     // 2.類內部創建對象,因為步驟3是static的,
    // 所以實例對象是static的
    private final static HungrySingleton instance = new HungrySingleton();

    //3.對外提供一個獲取對象的方法,
    // 因為調用方式的目的就是為了獲取對象,
    // 所以該方法應該是static的。
    public static HungrySingleton getInstance() {
        return instance;
    }
}

運行程式顯示,我們的確只創建了一個對象實例。

小結

優點:代碼實現比較簡單,在類載入的時候就完成了實例化,同時,該方式能夠避免線程安全問題。
缺點:在類裝載的時候就完成實例化,沒有達到Lazy Loading的效果。如果從始至終從未使用過這個實例,則會造成記憶體的浪費。
這種方式基於classloder機制避免了多線程的同步問題,不過, instance在類裝載時就實例化,在單例模式中大多數都是調用getInstance方法, 但是導致類裝載的原因有很多種, 因此不能確定有其他的方式(或者其他的靜態方法)導致類裝載,這時候初始化instance就沒有達到lazy loading的效果。
總結:這種單例模式可以使用,但是可能造成記憶體的浪費。

餓漢式(靜態代碼塊)

該方式和第一種區別不大,只是將創建實例放在了靜態代碼塊中。
由於無法使用new,你應該考慮提供一個獲取單例對象的方式給別人。

思路

1.將構造器私有化(防止外部new,但是對反射還是有局限)
2.類的內部創建對象(通過靜態代碼塊)
3.對外提供一個獲取實例靜態的public方法

代碼實現:

public class Singleton2 {
    public static void main(String[] args) {
        HungrySingleton hungrySingleton = HungrySingleton.getInstance();
        HungrySingleton hungrySingleton1 = HungrySingleton.getInstance();
        System.out.println(hungrySingleton == hungrySingleton1);
    }
}

class HungrySingleton {
    //1.私有化構造器
    private HungrySingleton() {
    }

    // 2.類內部創建對象,因為步驟3是static的,
    // 所以實例對象是static的
    private final static HungrySingleton instance;

    static {
        instance = new HungrySingleton();
    }

    //3.對外提供一個獲取對象的方法,
    // 因為調用方式的目的就是為了獲取對象,
    // 所以該方法應該是static的。
    public static HungrySingleton getInstance() {
        return instance;
    }
}

小結

該方式只是將對象的創建放在靜態代碼塊中,其優點和缺點與第一種方式完全一樣。
總結:這種單例模式可以使用,但是可能造成記憶體的浪費。(同第一種)

懶漢式(線程不安全)

該方式的主要思想就是為了改善餓漢式的缺點,通過懶載入(在使用的時候再去載入),達到節約記憶體的目的。
由於無法使用new,你應該考慮提供一個獲取單例對象的方式給別人。

思路

1.將構造器私有化(防止外部new,但是對反射還是有局限)
2.類的內部創建對象,懶載入,在使用的時候才去載入
3.對外提供一個獲取實例靜態的public方法

代碼實現:

public class Singleton3 {
    public static void main(String[] args) {
        TestThread testThread = new TestThread();
        Thread thread = new Thread(testThread);
        Thread thread1 = new Thread(testThread);
        thread.start();
        thread1.start();
    }
}

class LazySingleton {
    //1.私有化構造器
    private LazySingleton() {}

    //2.類的內部聲明對象
    private volatile static LazySingleton instance;

    //3.對外提供獲取對象的方法
    public static LazySingleton getInstance() {
        //判斷類是否被初始化
        if (instance == null) {
            //第一次使用的時候,創建對象
            instance = new LazySingleton();
        }
        return instance;
    }
}

class TestThread implements Runnable {

    @Override
    public void run() {
        System.out.println("線程" + Thread.currentThread().getName() + "開始執行");
        try {
            //為了演示多線程情況
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        LazySingleton instance = LazySingleton.getInstance();
        System.out.println("線程" + Thread.currentThread().getName() + "初始化對象" + instance.hashCode());
    }
}

執行程式後,發現了問題:

//運行結果:
線程Thread-0開始執行
線程Thread-1開始執行
線程Thread-1初始化對象1391273746
線程Thread-0初始化對象547686109

小結

優點:起到了懶載入的作用,但是只能在單線程情況下使用。
缺點:多線程下不安全,如果一個線程進入到if語句中阻滯(還未開始創建對象),另一線程進入並通過了if判斷,則會創建多個實例,這一點就違背了單例的目的。
結論:實際情況下,不要使用這種方式。

懶漢式(線程安全,同步方法)

思路

同上一中方式一樣,但是為瞭解決多線程安全問題,使用同步方法。

代碼演示:

public class Singleton4 {
    public static void main(String[] args) {
        TestThread testThread = new TestThread();
        Thread thread = new Thread(testThread);
        Thread thread1 = new Thread(testThread);
        thread.start();
        thread1.start();
    }
}

class LazySingleton {
    //1.私有化構造器
    private LazySingleton() {}

    //2.類的內部聲明對象
    private volatile static LazySingleton instance;

    //3.對外提供獲取對象的方法
    public synchronized static LazySingleton getInstance() {
        //判斷類是否被初始化
        if (instance == null) {
            //第一次使用的時候,創建對象
            instance = new LazySingleton();
        }
        return instance;
    }
}

class TestThread implements Runnable {

    @Override
    public void run() {
        System.out.println("線程" + Thread.currentThread().getName() + "開始執行");
        try {
            //為了演示多線程情況
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        LazySingleton instance = LazySingleton.getInstance();
        System.out.println("線程" + Thread.currentThread().getName() + "初始化對象" + instance.hashCode());
    }
}

運行結果如下所示:

線程Thread-1開始執行
線程Thread-0開始執行
線程Thread-0初始化對象681022576
線程Thread-1初始化對象681022576

小結

優點:起到了懶載入的效果,同時,解決了線程安全問題。
缺點:效率低下,每次想要獲取對象的時候,去執行getInstance()都是通過同步方法。而且,初始化對象後,再次使用的時候,應該直接return這個對象。
總結:可以在多線程條件下使用,但是效率低下,不推薦。

懶漢式(線程安全,同步代碼塊)

思路

同樣是為瞭解決多線程安全問題,不過採用的是同步代碼塊。首先,最先想到的是:

1.將getInstance()方法體全部加上同步鎖。

代碼實現:

public class Singleton5 {
    public static void main(String[] args) {
        TestThread testThread = new TestThread();
        Thread thread = new Thread(testThread);
        Thread thread1 = new Thread(testThread);
        thread.start();
        thread1.start();
    }
}

//對getInstance()的方法體整體加同步代碼塊
class LazySingleton {
    //1.私有化構造器
    private LazySingleton() {}

    //2.類的內部聲明對象
    private volatile static LazySingleton instance;

    //3.對外提供獲取對象的方法
    public static LazySingleton getInstance() {
        //同步代碼塊
        synchronized (LazySingleton.class) {
            //判斷類是否被初始化
            if (instance == null) {
                //第一次使用的時候,創建對象
                instance = new LazySingleton();
            }
        }
        return instance;
    }
}

class TestThread implements Runnable {

    @Override
    public void run() {
        System.out.println("線程" + Thread.currentThread().getName() + "開始執行");
        try {
            //為了演示多線程情況
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        LazySingleton instance = LazySingleton.getInstance();
//     LazySingleton1 instance = LazySingleton1.getInstance();
        System.out.println("線程" + Thread.currentThread().getName() + "初始化對象" + instance.hashCode());
    }
}

運行的結果:

線程Thread-0開始執行
線程Thread-1開始執行
線程Thread-1初始化對象1419349448
線程Thread-0初始化對象1419349448

這種方式的優缺點和同步方法一樣,能夠實現多線程安全,但是效率低下。那麼,能不能提高一下效率呢?我們發現,每次調用getInstance()的時候,都要進入同步代碼塊,但是,一旦對象初始化後,第二次使用的時候,應該能夠直接獲取這個對象才對。
按照這個思路,對代碼進行更改(為了說明這個,新建一個類LazySingleton1):

2.只在初始化對象部分加上同步鎖

代碼實現:

//為了提高效率,通過if判斷,初始化之前進入同步鎖
class LazySingleton1 {
    //1.私有化構造器
    private LazySingleton1() {}

    //2.類的內部聲明對象
    private volatile static LazySingleton1 instance;

    //3.對外提供獲取對象的方法
    public static LazySingleton1 getInstance() {
        //判斷類是否被初始化
        if (instance == null) {
            //第一次使用的時候,創建對象
            synchronized (LazySingleton1.class) {
                instance = new LazySingleton1();
            }
        }
        return instance;
    }

將類TestClass的run()方法進行更改,獲取的實例改為LazySingleton1類型。代碼看上去沒有問題,那麼運行效果如何呢:

//運行結果:
線程Thread-1開始執行
線程Thread-0開始執行
線程Thread-1初始化對象1368942806
線程Thread-0初始化對象1187311731

那麼,我們發現,打臉了,多線程情況下,創建了兩個對象,並未達到單例的目的。

小結

  • 對整個方法體加同步代碼塊
    可以達到要求,優缺點同同步方法。
  • 只在初始化對象的代碼添加同步鎖
    不能滿足線程安全要求,實際工作中,不能使用這種方式

懶漢式(線程安全,雙重檢查機制)

思路

針對懶漢式的多線程問題,我們可謂是操碎了心:同步方法可以解決問題,但是效率太低了;同步代碼塊則根本不能保證多線程安全。如何能做到“魚和熊掌兼得”呢?既然同步代碼塊的效率較好,那麼我們就針對這個方式進行改良:雙重檢查機制,即在getInstance()內進行兩次檢查,第一次通過if判斷後,初始化對象之前,進行同步並再次進行判斷。這樣做的目的:既能解決線程安全問題,同時避免第二次使用對象的時候還要執行同步的代碼。

代碼實現:

public class Singleton6 {
    public static void main(String[] args) {
        TestThread testThread = new TestThread();
        Thread thread = new Thread(testThread);
        Thread thread1 = new Thread(testThread);
        thread.start();
        thread1.start();
    }
}

class LazyDoubleCheckSingleton {
    //1.私有化構造器
    private LazyDoubleCheckSingleton() {}

    //2.類的內部聲明對象
    private volatile static LazyDoubleCheckSingleton instance;

    //3.對外提供獲取對象的方法
    public static LazyDoubleCheckSingleton getInstance() {
        //判斷類是否被初始化
        if (instance == null) {
            //第一次使用,通過if判斷
            //加鎖
            synchronized (LazyDoubleCheckSingleton.class) {
                //拿到鎖後,初始化對象之前,再次進行判斷
                if (instance == null) {
                    instance = new LazyDoubleCheckSingleton();
                }
            }
        }
        return instance;
    }
}

class TestThread implements Runnable {

    @Override
    public void run() {
        System.out.println("線程" + Thread.currentThread().getName() + "開始執行");
        try {
            //為了演示多線程情況
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        LazyDoubleCheckSingleton instance = LazyDoubleCheckSingleton.getInstance();
        System.out.println("線程" + Thread.currentThread().getName() + "初始化對象" + instance.hashCode());
    }
}

運行結果如下所示:

//運行結果:
線程Thread-0開始執行
線程Thread-1開始執行
線程Thread-1初始化對象996963733
線程Thread-0初始化對象996963733

小結

優點:

  • 解決了上一種方式中的線程安全問題,同時實現了延遲載入的效果,節約記憶體;
  • 第二次使用的時候,if判斷為false,直接返回創建好的對象,避免進入同步代碼,提高了效率;
    結論:推薦使用這種方式,實際工作中也比較常見這種方式。

靜態內部類

思路

為了實現多線程情況下安全,除了手工加鎖,還有別的方式。現在,我們採用靜態內部類的方式。這種方式利用了JVM載入類的機制來保證只初始化一個對象。
思路同樣是私有化構造器,對外提供靜態的公開方法;不同之處是,類的創建交給靜態內部類來時實現。

代碼實現

public class Singleton7 {
    public static void main(String[] args) {
        TestThread testThread = new TestThread();
        Thread thread = new Thread(testThread);
        Thread thread1 = new Thread(testThread);
        thread.start();
        thread1.start();
    }
}

class StaticInnerSingleton {
    // 1.構造器私有化
    private StaticInnerSingleton() {}

    // 2.通過靜態內部類來初始化對象
    private static class InnerClass {
        private static final StaticInnerSingleton INSTANCE = new StaticInnerSingleton();
    }

    // 3.對外提供獲取對象的方法
    public static StaticInnerSingleton getInstance() {
        return InnerClass.INSTANCE;
    }
}


class TestThread implements Runnable {

    @Override
    public void run() {
        System.out.println("線程" + Thread.currentThread().getName() + "開始執行");
        try {
            //為了演示多線程情況
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        StaticInnerSingleton instance = StaticInnerSingleton.getInstance();
        System.out.println("線程" + Thread.currentThread().getName() + "初始化對象" + instance.hashCode());
    }
}

運行結果:

線程Thread-0開始執行
線程Thread-1開始執行
線程Thread-0初始化對象1326533480
線程Thread-1初始化對象1326533480

OK,我們發現,這種方式達到了預期的效果。

小結

優點:

  • 這種靜態內部類的方式,通過類載入機制來保證了初始化實例時只有一個實例。
  • 類的靜態屬性只有在第一次載入類的時候初始化,而JVM能保證線程安全,在類的初始化過程中,只有一個線程能進入並完成初始化。
  • 靜態內部類方式實現了懶載入的效果,這種方式不會在類StaticInnerSingleton載入的時候進行初始化,而是在第一次使用時調用getInstance()方法初始化,能夠起到節約內次的目的。
  • 該方式的getInstance()方法,通過調用靜態內部類的靜態屬性返回實例對象,避免了每次調用時進行同步,效率高。
    結論:線程安全,效率高,代碼實現簡單,推薦使用。

枚舉

思路

在靜態內部類的方式中,我們借用了JVM的類載入機制來實現了功能,同樣,還可以借用Java的枚舉來實現單例模式。

public class Singleton8 {
    public static void main(String[] args) {
        TestThread testThread = new TestThread();
        Thread thread = new Thread(testThread);
        Thread thread1 = new Thread(testThread);
        thread.start();
        thread1.start();
    }
}

enum EnumSingleton {
    INSTANCE;

    public void sayHi() {
        System.out.println("Hi, " + INSTANCE);
    }
}

class TestThread implements Runnable {

    @Override
    public void run() {
        System.out.println("線程" + Thread.currentThread().getName() + "開始執行");
        try {
            //為了演示多線程情況
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        EnumSingleton instance = EnumSingleton.INSTANCE;
        System.out.println("線程" + Thread.currentThread().getName() + "初始化對象" + instance.hashCode());
    }
}

運行結果如下:

線程Thread-0開始執行
線程Thread-1開始執行
線程Thread-1初始化對象1134798663
線程Thread-0初始化對象1134798663

小結

優點:這中方式需要在JDK1.5以上的版本中使用,利用枚舉來實現單例模式。不僅能避免多線程同步問題,而且還能防止反序列化重新創建新的對象。在《Effective Java》中提到了這種方式,其作者推薦。
結論:推薦使用。

單例模式的序列化漏洞

在上面的枚舉類的總結中,我們提高枚舉方式能夠避免反序列化對象的時候重新建立新的對象(反序列化漏洞),那麼什麼是反序列化漏洞呢?Java對象進行反序列化的時候會通過反射機制來創建實例,反射機制的存在使得我們可以越過Java本身的靜態檢查和類型約束,在運行期直接訪問和修改目標對象的屬性和狀態。這裡理解的不是很準確,有錯誤的話請指出。

代碼演示:

public class Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
//        HungrySingleton instance = HungrySingleton.getInstance();
//        //序列化
//        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("serializable_singleton"));
//        oos.writeObject(instance);
//
//        //反序列化
//        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("serializable_singleton"));
//        HungrySingleton newInstance = (HungrySingleton) ois.readObject();

        LazyDoubleCheckSingleton instance = LazyDoubleCheckSingleton.getInstance();
        //序列化
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("serializable_singleton"));
        oos.writeObject(instance);

        //反序列化
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("serializable_singleton"));
        LazyDoubleCheckSingleton newInstance = (LazyDoubleCheckSingleton) ois.readObject();

        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }

}
class HungrySingleton implements Serializable {


    private static final long serialVersionUID = -4913346286867374832L;

    //1.私有化構造器
    private HungrySingleton() {
    }

    // 2.類內部創建對象,因為步驟3是static的,
    // 所以實例對象是static的
    private final static HungrySingleton instance;

    static {
        instance = new HungrySingleton();
    }

    //3.對外提供一個獲取對象的方法,
    // 因為調用方式的目的就是為了獲取對象,
    // 所以該方法應該是static的。
    public static HungrySingleton getInstance() {
        return instance;
    }

    //解決單例模式的反序列化漏洞
  //    public Object readResolve() {
//        return instance;
//    }
}

class LazyDoubleCheckSingleton implements Serializable {
    private static final long serialVersionUID = -8459475238793042042L;

    //1.私有化構造器
    private LazyDoubleCheckSingleton() {}

    //2.類的內部聲明對象
    private volatile static LazyDoubleCheckSingleton instance;

    //3.對外提供獲取對象的方法
    public static LazyDoubleCheckSingleton getInstance() {
        //判斷類是否被初始化
        if (instance == null) {
            //第一次使用,通過if判斷
            //加鎖
            synchronized (LazyDoubleCheckSingleton.class) {
                //拿到鎖後,初始化對象之前,再次進行判斷
                if (instance == null) {
                    instance = new LazyDoubleCheckSingleton();
                }
            }
        }
        return instance;
    }

//    public Object readResolve() {
//        return instance;
//    }
}

這裡,我們分別提供了懶漢式和餓漢式(雙重檢查)來驗證這個現象。運行後會報錯,實現Serializable介面後能夠正常運行,結果如下:

com.bm.desginpattern.pattern.creational.singleton.serialization.LazyDoubleCheckSingleton@7f31245a
com.bm.desginpattern.pattern.creational.singleton.serialization.LazyDoubleCheckSingleton@6d03e736
false

創建了兩個對象,沒有實現多線程安全。首先說明一下解決方案,然後再講解一下原理。我們發現餓漢式還是懶漢式都新增了一個方法readResolve(),將註釋取消後,再次運行的結果如下:

com.bm.desginpattern.pattern.creational.singleton.serialization.LazyDoubleCheckSingleton@7f31245a
com.bm.desginpattern.pattern.creational.singleton.serialization.LazyDoubleCheckSingleton@7f31245a
true

奇跡出現了,只是增加一個方法,情況完全不同了。那麼背後的原理是什麼呢?我們通過debug來講解:

1.在23行打一個斷點,進入併進入該方法:

2.我們發現,該方法首先是進行一些判斷,然後執行readObject0()方法,進入該方法查看:

//該方法完成代碼
 private Object readObject0(boolean unshared) throws IOException {
        boolean oldMode = bin.getBlockDataMode();
        if (oldMode) {
            int remain = bin.currentBlockRemaining();
            if (remain > 0) {
                throw new OptionalDataException(remain);
            } else if (defaultDataEnd) {
                /*
                 * Fix for 4360508: stream is currently at the end of a field
                 * value block written via default serialization; since there
                 * is no terminating TC_ENDBLOCKDATA tag, simulate
                 * end-of-custom-data behavior explicitly.
                 */
                throw new OptionalDataException(true);
            }
            bin.setBlockDataMode(false);
        }

        byte tc;
        while ((tc = bin.peekByte()) == TC_RESET) {
            bin.readByte();
            handleReset();
        }

        depth++;
        totalObjectRefs++;
        try {
            switch (tc) {
                case TC_NULL:
                    return readNull();

                case TC_REFERENCE:
                    return readHandle(unshared);

                case TC_CLASS:
                    return readClass(unshared);

                case TC_CLASSDESC:
                case TC_PROXYCLASSDESC:
                    return readClassDesc(unshared);

                case TC_STRING:
                case TC_LONGSTRING:
                    return checkResolve(readString(unshared));

                case TC_ARRAY:
                    return checkResolve(readArray(unshared));

                case TC_ENUM:
                    return checkResolve(readEnum(unshared));

                case TC_OBJECT:
                    return checkResolve(readOrdinaryObject(unshared));

                case TC_EXCEPTION:
                    IOException ex = readFatalException();
                    throw new WriteAbortedException("writing aborted", ex);

                case TC_BLOCKDATA:
                case TC_BLOCKDATALONG:
                    if (oldMode) {
                        bin.setBlockDataMode(true);
                        bin.peek();             // force header read
                        throw new OptionalDataException(
                            bin.currentBlockRemaining());
                    } else {
                        throw new StreamCorruptedException(
                            "unexpected block data");
                    }

                case TC_ENDBLOCKDATA:
                    if (oldMode) {
                        throw new OptionalDataException(true);
                    } else {
                        throw new StreamCorruptedException(
                            "unexpected end of block data");
                    }

                default:
                    throw new StreamCorruptedException(
                        String.format("invalid type code: %02X", tc));
            }
        } finally {
            depth--;
            bin.setBlockDataMode(oldMode);
        }
    }

我們發現,該方法還是對傳入的對象進行一些判斷,在這裡,我們匹配到TC_OBJECT,執行對應的方法。
3.進入該方法:

4.進一步查看:

我們看到一個名為resolveEx的屬性,說明很接近了。
5.繼續往下調試:


我們發現,這三個條件都滿足,因為我們在LazyDoubleCheckSingleton類中定義了readResolve()方法。

6.if判斷通過,進入到下一個方法:

7.在該方法中,我們發現經過一些條件判斷後,通過反射方式來調用我們在類LazyDoubleCheckSingleton中新定義的方法readResolve():

  • 如果我們沒有新增這個方法,反射的時候會新建一個LazyDoubleCheckSingleton對象,並將其返回;
  • 當我們新增這個readResolve()的時候,反射的時候還是會創建一個新的對象,但是,返回的是我們在readResolve()中的定義的返回對象。從而達到了多線程安全的目的。

單例模式的反射

未完待續


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • Vue+Typescript中在Vue上掛載axios使用時報錯 在 項目開發過程中,為了方便在各個組件中調用 ,我們通常會在入口文件將axios掛載到vue原型身上,如下: 這樣的話,我們在各個組件中進行請求時,就可以直接使用 ,但是在ts中使用 進行請求時,會進行報錯,如下所示: 從圖中我們可以 ...
  • 一、小程式概述 2017 年 1 月 9 日小程式正式上線,騰訊開放了個人開發者開發小程式,小程式從此就開始火爆,這一年,小程式狂攬 4 億用戶、1.7 億的日常活躍,上線 58 萬個。這是一個巨大的機會,對於企業宣傳,拉新用戶存在變革性的影響。 小程式的本質是:輕應用,就是不用安裝就能使用的手機A ...
  • 最近在研究MUI框架時,看到這樣的代碼: init,這個外面還有一層: 有些明瞭了,這就像java中對象的裡面定義了一個init方法,這裡init方法也是初始化方法,這裡是對整個頁面進行初始化。調用類似java:_App.init()即可. 關於細節說法:這是一種json寫法,把所有的functio ...
  • 下載NEditor 放在 vue 項目下麵 public 文件中。 安裝 vue-neditor-wrap 執行命令 npm install vue-neditor-wrap 代碼使用 <VueNeditorWrap ref="VueNeditorWrap" v-model="content" :c ...
  • 摘要: 理解函數式編程。 作者:前端小智 原文: "JS中函數式編程基本原理簡介" "Fundebug" 經授權轉載,版權歸原作者所有。 在長時間學習和使用面向對象編程之後,咱們退一步來考慮系統複雜性。 在做了一些研究之後,我發現了函數式編程的概念,比如不變性和純函數。這些概念使你能夠構建無副作用的 ...
  • jTopo 幫助說明網站 http://www.jtopo.com/index.html 使用例子: http://www.jtopo.com/demo/helloworld.html 不建議直接安裝 github 上的代碼,因為代碼版本不是最新,有部分功能未實現。 下載最新的js類庫文件放到 vu ...
  • vis.js 網站 https://visjs.org/ vs code 下安裝命令 npm install vis-network 在vue 下引入 vis-network組件 const vis = require("vis-network/dist/vis-network.min.js"); ...
  • ## DOM簡單學習:為了滿足案例要求 * 功能:控制html文檔的內容 * 獲取頁面標簽(元素)對象:Element * document.getElementById("id值"):通過元素的id獲取元素對象 * 操作Element對象: 1. 修改屬性值: 1. 明確獲取的對象是哪一個? 2.... ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...