单例模式
其它创建型模式链接:
1.单例模式概述
对于一个软件系统的某些类而言,只有一个实例很重要,例如一个系统只能有一个窗口管理器或文件系统,只能有一个计时工具等。
如何保证一个类只有一个实例并且这个实例易于被访问?定义一个统一的全局变量可以确保对象随时都可以被访问,但不能防止创建多个对象。一个更好的解决办法是让该类自身负责创建和保存唯一的实例,并保证不能创建其他实例,还要提供一个访问该实例的方法,这就是单例模式的动机。
单例模式定义如下:
单例模式:确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一实例。
单例模式的三个要点:
- 某个类只能有一个实例
- 它必须自行创建这个实例
- 它必须自行向整个系统提供这个实例
2.单例模式结构
单例模式只包含一个单例角色,也就是Singleton。
在单例类内部创建唯一实例,并通过getInstance方法让客户端可以使用这个实例,为防止在外部实例化,将构造函数可见性设置为private。
3.案例
某公司承接了一个服务器负载均衡软件的开发工作,该软件运行在一个负载均衡服务器上。由于集群中的服务器需要动态删减,并且客户端请求需要统一分发,因此需要确保负载均衡服务器的唯一性。使用单例模式设计负载均衡服务器。
3.1结构图
3.2代码实现
单例类
public class LoadBalancer {
//存储唯一实例
private static LoadBalancer instance = null;
//服务器集合
private List<String> serverList = null;
//私有构造函数
private LoadBalancer() {
serverList = new ArrayList<>();
}
//公有静态成员方法,返回唯一实例
public static LoadBalancer getLoadBalancer() {
if (instance == null) {
instance = new LoadBalancer();
}
return instance;
}
//增加服务器
public void addServer(String server) {
serverList.add(server);
}
//移除服务器
public void removeServer(String server) {
serverList.remove(server);
}
//使用Random类随机获取服务器
public String getServer() {
Random random = new Random();
int i = random.nextInt(serverList.size());
return serverList.get(i);
}
}
客户端
public class Demo {
public static void main(String[] args) {
LoadBalancer balancer1,balancer2;
balancer1=LoadBalancer.getLoadBalancer();
balancer2=LoadBalancer.getLoadBalancer();
if (balancer1==balancer2){
System.out.println("服务器负载均衡具有唯一性");
}
//增加服务器
balancer1.addServer("Server 1");
balancer1.addServer("Server 2");
balancer1.addServer("Server 3");
balancer1.addServer("Server 4");
//模拟发送请求
for (int i = 0; i < 10; i++) {
String server = balancer1.getServer();
System.out.println("请求发送至服务器 : "+server);
}
}
}
3.3效果展示
服务器负载均衡具有唯一性
请求发送至服务器 : Server 2
请求发送至服务器 : Server 1
请求发送至服务器 : Server 4
请求发送至服务器 : Server 2
请求发送至服务器 : Server 3
请求发送至服务器 : Server 4
请求发送至服务器 : Server 1
请求发送至服务器 : Server 3
请求发送至服务器 : Server 2
请求发送至服务器 : Server 2
4.饿汉式单例和懒汉式单例
4.1饿汉式单例
饿汉式单例实现起来最简单,结构图如下:
public class EagerSingleton {
private static final EagerSingleton instance = new EagerSingleton();
private EagerSingleton() {}
public static EagerSingleton getInstance() {
return instance;
}
}
当类被加载时,静态变量instance会被初始化,相当于类加载时单例对象就已创建。
4.2懒汉式单例与双检查锁定
与饿汉式相同的是构造函数也是私有的,但不同的是,懒汉式单例在第一次被引用时将自己实例化。
结构如下:
public class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton() {
}
synchronized public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
懒汉式单例在第一次调用getInstance()方法时,进行实例化,在类加载时并不实例化,这种技术又称为延迟加载技术,即需要时再加载实例。为了避免多个线程同时调用,可以使用synchronized关键字。
在上述模式中,虽然加了关键字进行线程锁定,解决了线程安全问题,但是每次调用都需要对线程进行锁定,在多线程高并发环境中会导致系统性能的大大降低。因此可以对其进行改造,只对关键代码进行锁定。
public static LazySingleton getInstance() {
if (instance == null) {
synchronized (LazySingleton.class){
instance = new LazySingleton();
}
}
return instance;
}
问题貌似得到解决,但事实并非如此,如果使用上述代码实现,还是会存在单例对象不唯一的情况。原因如下:
假如在某一瞬间线程A和B都在调用getInstance()方法,此时instance对象为null值,均能通过“instance==null“判断。由于实现了加锁机制,线程A进入synchronized锁定的代码中执行实例创建代码,线程B处于派对状态,必须等待线程A执行完。但是当A执行完后B并不知道实例已经创建,将继续创建新的实例,导致产生多个单例对象,违背了单例模式的设计思想。因此需要进一步改进,在synchronized中在进行一次instance是否为空的判断,这种方式为双重检查锁定(Double-Check Locking)。
改进后的实现如下:
public class LazySingleton {
private volatile static LazySingleton instance = null;
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (instance == null) {
synchronized (LazySingleton.class){
if (instance==null){
instance = new LazySingleton();
}
}
}
return instance;
}
}
需要注意的是,如果使用双重检查锁定来实现懒汉式单例类,需要在静态成员变量instance之前增加修饰符volatile,被volatile修饰的成员变量可以确保多个线程能够正常处理。由于volatile关键字会屏蔽java虚拟机所做的一些代码优化,可能会导致系统运行效率降低,因此双重检查锁定来实现单例模式也不是一种完美的实现方式。
4.3两种方式的比较
饿汉式单例类在被加载时就自己实例化,它的优点在于无需考虑多个线程同时访问的问题,可以确保实例的唯一性;从调用速度和反应时间来讲,由于单例对象从一开始就得以创建,因此要优于懒汉式单例。但是在系统运行时无论是否需要使用该单例对象,由于在类加载时对象就需要创建,因此从资源利用效率角度来讲,饿汉式不及懒汉式单例,而且加载时间可能会比较长。
懒汉式单例类在第一次使用时创建,无须一直占用系统资源,实现了延迟加载,但是必须处理多个线程同时访问的问题,特别是当单例类作为资源控制器,在实例化时必然涉及到资源初始化,而资源初始化很有可能耗费大量时间,这意味着出现多线程,需要通过双重检查锁定机制等机制进行控制,这将导致系统性能收到影响。
4.4使用静态内部类实现单例模式
饿汉式单例类不能实现延迟加载,不管将来用不用始终占据内存;懒汉式单例类线程安全控制繁琐,而且性能受影响,可见,无论是饿汉式还是懒汉式都存在一些问题。为了客服这些问题,在Java语言中可以通过Initialization on Demand Holder(IoDH)技术来实现单例模式。
在IoDH中,需要在单例类中增加一个静态内部类,在该类中创建单例对象,再将该对象通过getInstance()方法返回给外部使用,实现代码如下:
public class Singleton {
private Singleton() {
}
private static class HolderClass {
private final static Singleton instance = new Singleton();
}
public static Singleton getInstance() {
return HolderClass.instance;
}
}
在上述代码中由于没有静态单例对象作为成员变量,因此类加载时不会进行实例化,第一次调用getInstance()时将加载内部静态类HolderClass,在该内部类中定义instance,此时会首先初始化这个成员变量,有Java虚拟机来保证其线程安全性,确保该变量只能初始化一次。
通过使用IoDH,皆可以实现延迟加载,又可以保证线程安全,不影响性能,不失为一种最好的Java语言单例模式实现方式;其缺点是与编程语言自身特性有关,很对面向对象语言并不支持IoDH。
5.单例模式优缺点与适用环境
单例模式作为一种目标明确、结构简单、理解容易的设计模式,在软件开发中的使用频率相当之高,在很多应用软件和框架中都得到广泛应用
5.1单例模式优点
单例模式的优点主要如下:
- 单例模式提供了对唯一实例的受控访问。因为单例模式封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
- 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。
- 允许可变数目的实例。基于单例模式可以进行扩展,使用与控制单例相似方法来获得指定个数的实例对象,既节省系统资源,又解决了由于单例对象共享过多有损性能的问题。
5.2单例模式缺点
单例模式的缺点主要如下:
- 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。‘
- 单例类的职责过重,在一定程度上违背了单一职责原则。因为单例类既提供了业务方法,又提供了创建对象的方法,将对象的创建和对象本身的功能耦合在一次。
- 现在很多面向对象语言的运行环境都提供了自动垃圾回收技术,因此如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。
5.3单例模式适用环境
在以下情况可以考虑使用单例模式:
- 系统只需要一个实例对象
单例模式中没有抽象层,因此单例类的扩展有很大的困难。‘ - 单例类的职责过重,在一定程度上违背了单一职责原则。因为单例类既提供了业务方法,又提供了创建对象的方法,将对象的创建和对象本身的功能耦合在一次。
- 现在很多面向对象语言的运行环境都提供了自动垃圾回收技术,因此如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。
5.3单例模式适用环境
在以下情况可以考虑使用单例模式:
- 系统只需要一个实例对象
- 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其它途径访问该实例。