1.概述
(1)單例模式 (Singleton Pattern) 是 Java 中最簡單的設(shè)計模式之一。它提供了一種創(chuàng)建對象的最佳方式。這種模式涉及到一個單一的類,該類負(fù)責(zé)創(chuàng)建自己的對象,同時確保只有單個對象被創(chuàng)建。這個類提供了一種訪問其唯一的對象的方式,可以直接訪問,不需要實例化該類的對象。
(2)單例模式的目的是限制一個類的實例數(shù)量,并提供全局訪問點以方便其他組件使用。它通常在需要共享資源、管理全局狀態(tài)或限制某個組件數(shù)量的情況下使用。然而,單例模式也有一些缺點,例如增加了代碼的耦合性和可擴展性的限制,因此在使用時需要權(quán)衡其利弊。
(3)單例模式的主要有單例類和訪問類這兩個角色:
單例類 | 只能創(chuàng)建一個實例的類 |
---|---|
訪問類 | 使用單例類 |
2.實現(xiàn)
單例模式分類兩種:餓漢式和懶漢式。
餓漢式 | 類加載就會導(dǎo)致該單實例對象被創(chuàng)建 |
---|---|
懶漢式 | 類加載不會導(dǎo)致該單實例對象被創(chuàng)建,而是首次使用該對象時才會創(chuàng)建 |
2.1.餓漢式
2.1.1.靜態(tài)變量
package com.itheima.patterns.singleton.hungryman1;
//餓漢式
public class Singleton{
//私有構(gòu)造方法
private Singleton(){}
//靜態(tài)變量創(chuàng)建類的對象
private static Singleton instance = new Singleton();
//對外提供靜態(tài)方法獲取該對象
public static Singleton getInstance(){
return instance;
}
}
說明: 方式一在成員位置聲明 Singleton 類型的靜態(tài)變量,并創(chuàng)建 Singleton 類的對象 instance。instance 對象是隨著類的加載而創(chuàng)建的。如果該對象足夠大的話,而一直沒有使用就會造成內(nèi)存的浪費。
2.1.2.靜態(tài)代碼塊
package com.itheima.patterns.singleton.hungryman2;
public class Singleton{
//私有構(gòu)造方法
private Singleton(){}
//在靜態(tài)代碼塊中進行創(chuàng)建
private static Singleton instance;
static {
instance = new Singleton();
}
//對外提供靜態(tài)方法獲取該對象
public static Singleton getInstance(){
return instance;
}
}
說明:方式二在成員位置聲明Singleton類型的靜態(tài)變量,而對象的創(chuàng)建是在靜態(tài)代碼塊中,也是隨著類的加載而創(chuàng)建。所以和方式一基本一樣,也存在內(nèi)存浪費問題。
2.1.3.枚舉方式
enum Singleton {
//INSTANCE;
INSTANCE("Tom", 21);
// 添加其他成員變量
private String name;
private int age;
Singleton(String name, int age) {
this.name = name;
this.age = age;
}
public void setName(String name) {
this.name = name;
}
public String getName() {
return this.name;
}
public void setAge(int age) {
this.age = age;
}
public int getAge() {
return this.age;
}
// 添加其他方法
public void sayHello() {
System.out.println("Hello, I'm " + name + ", " + age + " years old.");
}
}
說明:枚舉類實現(xiàn)單例模式是極力推薦的單例實現(xiàn)模式,因為枚舉類型是線程安全的,并且只會裝載一次,設(shè)計者充分的利用了枚舉的這個特性來實現(xiàn)單例模式,枚舉的寫法非常簡單,而且枚舉類型是所用單例實現(xiàn)中唯一一種不會被破壞的單例實現(xiàn)模式。測試代碼如下:
Client.java
public class Client {
public static void main(String[] args) {
Singleton instance1 = Singleton.INSTANCE;
Singleton instance2 = Singleton.INSTANCE;
System.out.println("instance1 == instance2 的結(jié)果為: " + (instance1 == instance2)); // true
System.out.println(instance1.getAge());
instance1.setAge(50);
System.out.println(instance2.getAge());
}
}
輸出結(jié)果如下:
instance1 == instance2 的結(jié)果為: true
21
50
2.2.懶漢式
2.2.1.synchronized 線程安全
package com.itheima.patterns.singleton.Lazyman;
public class Singleton{
//私有構(gòu)造方法
private Singleton(){}
//聲明 Singleton 類型的變量 instance,并未進行賦值
private static Singleton instance;
//使用關(guān)鍵字 synchronized 的目的在于保證線程安全
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
說明:該方式實現(xiàn)了懶加載效果,同時又解決了線程安全問題。但是在 getInstance() 方法上添加了 synchronized 關(guān)鍵字,導(dǎo)致該方法的執(zhí)行效果特別低。從上面代碼我們可以看出,其實就是在初始化 instance 的時候才會出現(xiàn)線程安全問題,一旦初始化完成就不存在了。
2.2.2.雙重檢查鎖
public class Singleton {
//私有構(gòu)造方法
private Singleton(){}
//聲明 Singleton 類型的變量 instance,并未進行賦值
private static volatile Singleton instance;
public static Singleton getInstance(){
//第一次檢查,若 instance 不為 null,則不進入搶鎖階段,直接返回實際值即可
if (instance == null) {
synchronized(Singleton.class){
//第二次檢查,得到鎖之后再次判斷 instance 是否為空
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
(1)雙重檢驗鎖 (Double-Checked Locking) 是一種在多線程環(huán)境下實現(xiàn)單例模式的優(yōu)化方式。其主要原理如下:
- 首先檢查實例是否已經(jīng)被創(chuàng)建,如果已經(jīng)創(chuàng)建,則直接返回已經(jīng)創(chuàng)建的實例,不再進入同步代碼塊,提高性能。
- 如果實例尚未創(chuàng)建,則進入同步代碼塊,在同步代碼塊內(nèi)再次檢查實例是否已經(jīng)被創(chuàng)建。
- 在同步代碼塊內(nèi)部進行第二次檢查時,由于進入同步塊的只有一個線程,其他線程處于等待狀態(tài),避免了多個線程同時進入同步代碼塊創(chuàng)建實例。
- 在第二次檢查時,如果實例尚未創(chuàng)建,則創(chuàng)建實例,并將實例賦值給成員變量,確保只有一個實例被創(chuàng)建。
- 最后,釋放鎖,并返回實例。
(2)這種方式結(jié)合了懶加載和線程安全的特點,在第一次調(diào)用時才創(chuàng)建實例,在多線程環(huán)境下確保只有一個實例被創(chuàng)建。雙重檢查鎖模式是一種非常好的單例實現(xiàn)模式,解決了單例、性能、線程安全問題。
(3)在雙重檢查鎖的實現(xiàn)方式中,如果沒有使用 volatile
關(guān)鍵字,可能會出現(xiàn)指令重排序的問題。具體來說,當(dāng)一個線程進入第一個 if
判斷時,如果實例還未被創(chuàng)建,那么它會獲取鎖并創(chuàng)建實例。但是由于指令重排序的影響,實例的初始化可能會在獲取鎖之前被重排序到鎖的后面,這就導(dǎo)致其他線程在第二個 if 判斷中認(rèn)為實例已經(jīng)創(chuàng)建,從而返回一個未完全初始化的實例。
(4)通過使用 volatile 關(guān)鍵字修飾 instance 變量,可以保證在多線程環(huán)境下對 instance 的讀取和寫入操作都是有序的,避免了指令重排序問題,并且保證了其他線程能夠正確地看到已經(jīng)完全初始化的實例。
相關(guān)知識點:
Java 并發(fā)編程面試題——synchronized 與 volatile
2.2.3.靜態(tài)內(nèi)部類方式
package com.itheima.patterns.singleton.lazyman3;
public class Singleton {
//私有構(gòu)造方法
private Singleton(){}
//靜態(tài)內(nèi)部類
private static class SingletonHolder{
private static final Singleton INSTANCE = new Singleton();
}
//對外提供靜態(tài)方法獲取該對象
public static Singleton getInstance(){
return SingletonHolder.INSTANCE;
}
}
(1)第一次加載 Singleton 類時不會去初始化 INSTANCE,只有第一次調(diào)用 getInstance() 時,虛擬機才加載 SingletonHolder,并初始化 INSTANCE,這樣不僅能確保線程安全,也能保證 Singleton 類的唯一性??傊?,靜態(tài)內(nèi)部類單例模式是一種優(yōu)秀的單例模式,是開源項目中比較常用的一種單例模式。在沒有加任何鎖的情況下,保證了多線程下的安全,并且沒有任何性能影響和空間的浪費。
(2)靜態(tài)內(nèi)部類實現(xiàn)的單例可以做到線程安全且可延遲加載的原因如下:
- 線程安全:靜態(tài)內(nèi)部類的加載是在第一次使用時進行的,并且由 Java 虛擬機來保證類加載的線程安全性。在類加載過程中,虛擬機會對類進行初始化,并且只會進行一次,這樣就避免了多線程環(huán)境下的競爭條件。因此,通過靜態(tài)內(nèi)部類實現(xiàn)的單例可以在多線程環(huán)境下安全地被多個線程共享。
- 可延遲加載:靜態(tài)內(nèi)部類的初始化是在第一次使用時進行的,即在調(diào)用 getInstance 方法時才會加載內(nèi)部類。這樣可以延遲單例對象的初始化過程,只有在需要使用到單例對象時才進行初始化。這對于資源消耗較大的對象或需求延遲加載的場景特別有用,可以節(jié)省內(nèi)存空間和系統(tǒng)資源,并提高應(yīng)用的啟動性能。
3.破壞單例模式
破壞單例模式演示(序列化反序列化和反射)
3.1.序列化反序列化
package com.itheima.patterns.singleton.problem1;
import java.io.Serializable;
public class Singleton implements Serializable {
//私有構(gòu)造方法
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//對外提供靜態(tài)方法獲取該對象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
package com.itheima.patterns.singleton.problem1;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
public class Client {
public static void main(String[] args) throws Exception {
writeObject2File();
readObjectFromFile();
readObjectFromFile();
}
//從文件讀取數(shù)據(jù)(對象)
public static void readObjectFromFile() throws Exception {
//1.創(chuàng)建對象輸入流對象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("E:\\testData\\a.txt"));
//2.讀取對象
Singleton instance = (Singleton) ois.readObject();
System.out.println(instance);
//釋放資源
ois.close();
}
//向文件中寫數(shù)據(jù)(對象)
public static void writeObject2File() throws Exception {
//1.獲取Singleton對象
Singleton instance = Singleton.getInstance();
//2.創(chuàng)建對象輸出流對象
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("E:\\testData\\a.txt"));
//3.寫對象
oos.writeObject(instance);
//4.釋放資源
oos.close();
}
}
3.2.反射
public class Singleton {
//私有構(gòu)造方法
private Singleton() {}
private static volatile Singleton instance;
//對外提供靜態(tài)方法獲取該對象
public static Singleton getInstance() {
if(instance != null) {
return instance;
}
synchronized (Singleton.class) {
if(instance != null) {
return instance;
}
instance = new Singleton();
return instance;
}
}
}
import java.lang.reflect.Constructor;
public class Client {
public static void main(String[] args) throws Exception {
//獲取 Singleton 類的字節(jié)碼對象
Class clazz = Singleton.class;
//獲取 Singleton 類的私有無參構(gòu)造方法對象
Constructor constructor = clazz.getDeclaredConstructor();
//取消訪問檢查
constructor.setAccessible(true);
//創(chuàng)建 Singleton 類的對象 s1
Singleton s1 = (Singleton) constructor.newInstance();
//創(chuàng)建 Singleton 類的對象 s2
Singleton s2 = (Singleton) constructor.newInstance();
//判斷通過反射創(chuàng)建的兩個 Singleton 對象是否是同一個對象
System.out.println(s1 == s2); // false
}
}
4.問題解決
(1)序列化、反序列化方式破壞單例模式的解決方法
在 Singleton 類中添加 readResolve() 方法,在反序列化時被反射調(diào)用,如果定義了這個方法,就返回這個方法的值,如果沒有定義,則返回新 new 出來的對象。
import java.io.Serializable;
public class Singleton implements Serializable {
//私有構(gòu)造方法
private Singleton() {}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//對外提供靜態(tài)方法獲取該對象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
//解決序列化反序列化破解單例模式
private Object readResolve() {
return SingletonHolder.INSTANCE;
}
}
具體的深入分析可以參考單例、序列化和 readResolve() 方法。
(2)反射方式破解單例的解決方法
當(dāng)通過反射方式調(diào)用構(gòu)造方法創(chuàng)建對象時,直接拋異常,不運行此種操作。
import java.io.Serializable;
public class Singleton implements Serializable {
private static boolean flag = false;
//私有構(gòu)造方法
private Singleton() {
synchronized (Singleton.class){
//若 flag 的值為 true,說明不是第一次訪問,直接拋一個異常
if (flag){
throw new RuntimeException("不能創(chuàng)建多個對象!");
}
flag = true;
}
}
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
//對外提供靜態(tài)方法獲取該對象
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
//解決序列化反序列化破解單例模式
private Object readResolve() {
return SingletonHolder.INSTANCE;
}
}
但是上面的操作也不一定安全,因為可以通過反射的方式來修改 flag
,最安全的方式應(yīng)該是使用枚舉方式來創(chuàng)建單例對象,其原因在于 JDK 底層在通過反射創(chuàng)建對象時,會檢查對象類型是否為枚舉類型,如果是,則會拋出 IllegalArgumentException
異常,從而創(chuàng)建對象失敗,這樣做的目的在于保證枚舉對象的單例性。具體相關(guān)源碼如下所示:
@CallerSensitive
public final class Constructor<T> extends Executable {
//...
public T newInstance(Object ... initargs)
throws InstantiationException, IllegalAccessException,
IllegalArgumentException, InvocationTargetException
{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, null, modifiers);
}
}
//檢查給定的 clazz 對象的修飾符中是否包含枚舉類型的標(biāo)志位,以確定其是否表示一個枚舉類型
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor; // read volatile
if (ca == null) {
ca = acquireConstructorAccessor();
}
@SuppressWarnings("unchecked")
T inst = (T) ca.newInstance(initargs);
return inst;
}
}
5.優(yōu)缺點
(1)單例模式是一種只允許創(chuàng)建一個實例的設(shè)計模式。它的主要優(yōu)點是:
- 獨一無二的實例:單例模式確保在整個應(yīng)用程序中只有一個實例存在。這在某些情況下是非常有用的,例如需要共享資源或限制系統(tǒng)中某個組件的數(shù)量。
- 全局訪問點:由于單例實例是全局可訪問的,所以可以方便地從任何地方訪問它。這對于需要頻繁訪問單例實例的組件非常有用,避免了傳遞實例的麻煩。
(2)然而,單例模式也有一些缺點:
- 耦合性:使用單例模式可能導(dǎo)致代碼中的緊耦合,因為它引入了全局狀態(tài)。這會使代碼在可測試性和可維護性方面變得更加困難,因為任何依賴單例實例的組件都與該實例緊密耦合。
- 難以擴展:由于單例模式只允許存在一個實例,因此在需要擴展功能時可能會遇到限制。如果需要創(chuàng)建多個實例來滿足新的需求,則需要修改單例類的實現(xiàn)。
- 隱藏依賴關(guān)系:使用單例模式可能會隱藏代碼中的依賴關(guān)系。由于可以從任何地方訪問單例實例,組件之間的依賴關(guān)系可能不明顯,導(dǎo)致代碼更難以理解和維護。
6.應(yīng)用場景
(1)單例模式可以應(yīng)用于許多場景,其中一些常見的應(yīng)用場景包括:
- 日志記錄器:在應(yīng)用程序中,通常只需要一個日志記錄器來記錄系統(tǒng)的日志信息。使用單例模式可以確保只有一個日志記錄器實例存在,并且可以從任何地方方便地訪問它。
- 數(shù)據(jù)庫連接池:在需要頻繁訪問數(shù)據(jù)庫的應(yīng)用程序中,可以使用單例模式來管理數(shù)據(jù)庫連接池。通過保持只有一個數(shù)據(jù)庫連接池實例存在,可以避免創(chuàng)建過多的數(shù)據(jù)庫連接,提高性能和資源利用率。
- 配置信息管理器:在應(yīng)用程序中,通常需要加載和訪問配置信息。使用單例模式可以創(chuàng)建一個全局的配置信息管理器,用于加載和提供應(yīng)用程序所需的配置信息,避免重復(fù)加載和管理多個實例。
- 線程池:在需要管理線程執(zhí)行的應(yīng)用程序中,可以使用單例模式來創(chuàng)建并管理線程池。通過保持只有一個線程池實例存在,可以方便地分配和管理線程,提高并發(fā)性能。
- GUI 應(yīng)用程序中的窗口管理器:在 GUI 應(yīng)用程序中,通常需要管理窗口的創(chuàng)建、顯示和銷毀。使用單例模式可以創(chuàng)建一個全局的窗口管理器,用于管理應(yīng)用程序中的所有窗口。
(2)這只是一些常見的應(yīng)用場景,實際上,任何需要全局訪問點和只允許存在一個實例的情況都可以考慮使用單例模式。但請注意,在使用單例模式時需要慎重考慮其優(yōu)缺點,并確保它滿足設(shè)計需求。
7.JDK 源碼解析——Runtime 類
(1)Runtime 類就是使用的單例設(shè)計模式,其部分源代碼如下:
public class Runtime {
private static Runtime currentRuntime = new Runtime();
/**
* Returns the runtime object associated with the current Java application.
* Most of the methods of class <code>Runtime</code> are instance
* methods and must be invoked with respect to the current runtime object.
*
* @return the <code>Runtime</code> object associated with the current
* Java application.
*/
public static Runtime getRuntime() {
return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {}
...
}
從上面源代碼中可以看出 Runtime
類使用的是餓漢式(靜態(tài)屬性)方式來實現(xiàn)單例模式的。文章來源:http://www.zghlxwxcb.cn/news/detail-546779.html
(2)使用 Runtime 類文章來源地址http://www.zghlxwxcb.cn/news/detail-546779.html
import java.io.IOException;
import java.io.InputStream;
public class RunTimeDemo {
public static void main(String[] args) throws IOException {
//獲取RunTime類對象
Runtime runtime = Runtime.getRuntime();
System.out.println("JVM 空閑內(nèi)存 =" + runtime.freeMemory() / (1024*1024) + "M");
System.out.println("JVM 總內(nèi)存 =" + runtime.totalMemory() / (1024*1024) + "M");
System.out.println("JVM 可用最大內(nèi)存 =" + runtime.maxMemory() / (1024*1024) + "M");
//調(diào)用 runtime 的方法 exec,參數(shù)為一個命令
Process process = runtime.exec("ipconfig");
//調(diào)用 process 對象的獲取輸入流的方法
InputStream is = process.getInputStream();
byte[] arr = new byte[1024 * 1024 * 100];
int length = is.read(arr);
System.out.println(new String(arr,0,length, "GBK"));
}
}
到了這里,關(guān)于Java 設(shè)計模式——單例模式的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!