共计 3751 个字符,预计需要花费 10 分钟才能阅读完成。
1 定义
有时候为了节约系统资源,须要确保零碎中某个类只有惟一一个实例,当这个实例创立胜利之后,无奈再创立一个同类型的其余对象,所有的操作都只能基于这个惟一的实例,这是单例模式的动机所在。
单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个零碎提供这个实例,这个类称为单例类,它提供全局拜访的办法。单例模式是一种对象创立型模式。
2 要点
- 某个类只能有一个实例
- 它必须自行创立这个实例
- 它必须自行向整个零碎提供这个实例
3 通用步骤
一般来说把一个一般类重构为一个单例类须要以下三步:
- 构造函数私有化:也就是禁止内部间接应用 new 等创建对象
- 定义一个动态类成员保留实例
- 减少一个相似
getInstance()
的私有静态方法来获取实例
因而一般来说单例模式的结构图如下:
4 实例
上面以一个简化的负载均衡器设计进行单例模式的阐明。
某个软件须要应用一个全局惟一的负载均衡器,设计如下:
public class LoadBalancer
{
private static LoadBalancer instance = null;
private LoadBalancer(){}
public static LoadBalancer getInstance()
{return instance == null ? instance = new LoadBalancer() : instance;
}
public static void main(String[] args) {LoadBalancer balancer1 = LoadBalancer.getInstance();
LoadBalancer balancer2 = LoadBalancer.getInstance();
System.out.println(balancer1 == balancer2);
}
}
这是最简略的单例类的设计,获取实例时仅仅判断是否为null
,没有思考到线程问题。也就是说,多个线程同时获取实例时,还是会产生多个实例,一般来说,常见的解决形式如下:
- 饿汉式单例
- 懒汉式单例
- IoDH
5 饿汉式单例
饿汉式单例就是在一般的单例类根底上,在定义动态变量时就间接实例化,因而在类加载的时候就曾经创立了单例对象,而且在获取实例时不须要进行判空操作间接返回实例即可:
public class LoadBalancer
{private static LoadBalancer instance = new LoadBalancer();
private LoadBalancer(){}
public static LoadBalancer getInstance()
{return instance;}
}
当类被加载时,动态变量 instance
被初始化,类的公有构造方法将被调用,单例类的惟一实例将被创立。
6 懒汉式单例
懒汉式单例在类加载时不进行初始化,在须要的时候再初始化,加载实例,同时为了防止多个线程同时调用getInstance()
,能够加上synchronized
:
public class LoadBalancer
{
private static LoadBalancer instance = null;
private LoadBalancer(){}
synchronized public static LoadBalancer getInstance()
{return instance == null ? instance = new LoadBalancer() : instance;
}
}
这种技术又叫提早加载技术,只管解决了多个线程同时拜访的问题,然而每次调用时都须要进行线程锁定判断,这样会升高效率。事实上,单例的外围在于instance = new LoadBalancer()
,因而只须要锁定这行代码,优化如下:
public static LoadBalancer getInstance()
{if(instance == null)
{synchronized (LoadBalancer.class)
{instance = new LoadBalancer();
}
}
return instance;
}
然而理论状况中还是有可能呈现多个实例,因为如果 A 和 B 两个线程同时调用 getInstance()
,都通过了if(instance == null)
的判断,假如线程 A 先取得锁,创立实例后,A 开释锁,接着 B 获取锁,再次创立了一个实例,这样还是导致产生多个单例对象,因而,通常采纳一种叫“双重查看锁定”的形式来确保不会产生多个实例:
private volatile static LoadBalancer instance = null;
public static LoadBalancer getInstance()
{if(instance == null)
{synchronized (LoadBalancer.class)
{if(instance == null)
{instance = new LoadBalancer();
}
}
}
return instance;
}
须要留神的是要应用 volatile
润饰变量,volatile
能够保障可见性以及有序性。
7 饿汉式与懒汉式的比拟
- 饿汉式单例在类加载时就曾经初始化,长处在于无需思考多线程拜访问题,能够确保实例的唯一性
- 从调用速度方面来说会优于懒汉式单例,因为在类加载时就曾经被创立
- 从资源利用效率来说饿汉式单例会劣于懒汉式,因为无论是否须要应用都会加载单例对象,而且因为加载时须要创立实例会导致类加载工夫变长
- 懒汉式单例实现了提早加载,毋庸始终占用系统资源
- 懒汉式单例须要解决多线程并发拜访问题,须要双重查看锁定,且通常来说初始化过程须要较长时间,会增大多个线程同时首次调用的几率,这会导致系统性能受肯定影响
8 IoDH
为了克服饿汉式单例不能提早加载以及懒汉式单例的线程安全控制繁琐问题,能够应用一种叫 Initialization on Demand Holder(IoDH)
的技术。实现 IoDH 时,需在单例类减少一个动态外部类,在该外部类中创立单例对象,再将该单例对象通过 getInstance()
办法返回给内部应用,代码如下:
public class LoadBalancer
{private LoadBalancer(){}
private static class HolderClass
{private static final LoadBalancer instance = new LoadBalancer();
}
public static LoadBalancer getInstance()
{return HolderClass.instance;}
}
因为单例对象没有作为 LoadBalancer
的成员变量间接实例化,因而类加载时不会实例化 instance
。首次调用getInstance()
时,会初始化 instance
,由 JVM 保障线程安全性,确保只能被初始化一次。另外相比起懒汉式单例,getInstance()
没有线程锁定,因而性能不会有任何影响。
通过 IoDH 既能够实现提早加载,又能够保障线程平安,不影响零碎性能,然而毛病是与编程语言自身的个性相干,很多面向对象语言不反对 IoDH。另外,还可能引发NoClassDefFoundError
(当初始化失败时),例子能够戳这里。
9 枚举实现单例(举荐)
其中,无论是饿汉式,还是懒汉式,还是 IoDH,都有或多或少的问题,并且还能够通过反射以及序列化 / 反序列化形式去“强制”生成多个单例,有没有更优雅的解决方案呢?
有!答案就是枚举。
代码如下:
public class Test
{public static void main(String[] args) {LoadBalancer balancer1 = SingletonEnum.INSTANCE.getInstance();
LoadBalancer balancer2 = SingletonEnum.INSTANCE.getInstance();
System.out.println(balancer1 == balancer2);
}
}
class LoadBalancer
{
}
enum SingletonEnum
{
INSTANCE;
private LoadBalancer instance = null;
private SingletonEnum()
{instance = new LoadBalancer();
}
public LoadBalancer getInstance()
{return instance;}
}
10 总结
10.1 长处
单例模式次要长处如下:
- 单例模式提供了对惟一实例的受控拜访,能够严格控制客户怎么以及何时拜访它
- 因为在零碎内存中只存在一个对象,因而能够节约系统资源,对于一些须要频繁创立和销毁的对象,单例模式能够进步零碎性能
- 能够扩大成多例类
10.2 毛病
单例模式次要毛病如下:
- 没有形象层,扩大艰难
- 单例类职责过重,肯定水平上违反了繁多权责准则,因为既提供了业务办法,也提供了创建对象办法,将对象创立以及对象自身的性能耦合在一起
- 很多语言提供了 GC 机制,实例化的对象长时间不应用将被回收,下次应用须要从新实例化,这回导致共享的单例对象状态失落
10.3 应用场景
- 零碎须要一个实例对象
- 客户调用类的单个实例只容许应用一个公共拜访点
10.4 思维导图总结