5、设计模式-创建型模式-单例模式
单例模式
经常遇到类似的情况,为了节约系统资源,有时需要确保系统中某
个类只有唯一一个实例,当这个唯一实例创建成功之后,我们无法再创建一个同类型的其他
对象,所有的操作都只能基于这个唯一实例。
为了确保对象的唯一性,我们可以通过单例模式来实现,这就是单例模式的动机所在。
假设任务管理器的类名为TaskManager,在
TaskManager类中包含了大量的成员方法,例如构造函数TaskManager(),显示进程的方法
displayProcesses(),显示服务的方法displayServices()等,该类的示意代码如下:
class TaskManager { public TaskManager() {……} //初始化窗口 public void displayProcesses() {……} //显示进程 public void displayServices() {……} //显示服务 …… }
(1) 由于每次使用new关键字来实例化TaskManager类时都将产生一个新对象,为了确保
TaskManager实例的唯一性,我们需要禁止类的外部直接使用new来创建对象,因此需要将
TaskManager的构造函数的可见性改为private,如下代码所示:
private TaskManager() {……}
(2) 将构造函数改为private修饰后该如何创建对象呢?不要着急,虽然类的外部无法再使用new
来创建对象,但是在TaskManager的内部还是可以创建的,可见性只对类外有效。因此,我们
可以在TaskManager中创建并保存这个唯一实例。为了让外界可以访问这个唯一实例,需要在
TaskManager中定义一个静态的TaskManager类型的私有成员变量,如下代码所示:
private static TaskManager tm = null;
(3) 为了保证成员变量的封装性,我们将TaskManager类型的tm对象的可见性设置为private,但
外界该如何使用该成员变量并何时实例化该成员变量呢?答案是增加一个公有的静态方法,
如下代码所示:
public static TaskManager getInstance() { if (tm == null) { tm = new TaskManager(); } return tm; }
在getInstance()方法中首先判断tm对象是否存在,如果不存在(即tm == null),则使用new关
键字创建一个新的TaskManager类型的tm对象,再返回新创建的tm对象;否则直接返回已有的
tm对象。
需要注意的是getInstance()方法的修饰符,首先它应该是一个public方法,以便供外界其他对象
使用,其次它使用了static关键字,即它是一个静态方法,在类外可以直接通过类名来访问,
而无须创建TaskManager对象,事实上在类外也无法创建TaskManager对象,因为构造函数是私
有的。
此时的完整代码:
class TaskManager { private static TaskManager tm = null; private TaskManager() {……} //初始化窗口 public void displayProcesses() {……} //显示进程 public void displayServices() {……} //显示服务 public static TaskManager getInstance() { if (tm == null) { tm = new TaskManager(); } return tm; } …… }
在类外我们无法直接创建新的TaskManager对象,但可以通过代码TaskManager.getInstance()来
访问实例对象,第一次调用getInstance()方法时将创建唯一实例,再次调用时将返回第一次创
建的实例,从而确保实例对象的唯一性。
定义如下:
确保某一个类只有一个实
例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方
法。单例模式是一种对象创建型模式。
三个要点:
一是某个类只能有一个实例
二是它必须自行创建这个实例
三是它必须自行向整个系统提供这个实例。
结构图:
只包含一个角色:
Singleton(单例):在单例类的内部实现只生成一个实例,同时它提供一个静态的
getInstance()工厂方法,让客户可以访问它的唯一实例;为了防止在外部对其实例化,将其构
造函数设计为私有;在单例类内部定义了一个Singleton类型的静态对象,作为外部共享的唯一
实例。
一个服务器负载均衡(Load Balance)软件的开发工作,该软件运行在一台
负载均衡服务器上,可以将并发访问和数据流量分发到服务器集群中的多台设备上进行并发
处理,提高系统的整体处理能力,缩短响应时间。
由于集群中的服务器需要动态删减,且客
户端请求需要统一分发,因此需要确保负载均衡器的唯一性,只能有一个负载均衡器来负责
服务器的管理和请求的分发,否则将会带来服务器状态的不一致以及请求分配冲突等问题
服务器负载均衡结构图:
public class LoadBalancer { //私有静态成员变量,存储唯一实例 private static LoadBalancer instance = null; //服务器集合 private List 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 (String) serverList.get(i); } }
public static void main(String[] args) { //创建4个LoadBalancer对象 LoadBalancer balancer1,balancer2,balancer3,balancer4; balancer1 = LoadBalancer.getLoadBalancer(); balancer2 = LoadBalancer.getLoadBalancer(); balancer3 = LoadBalancer.getLoadBalancer(); balancer4 = LoadBalancer.getLoadBalancer(); //判断服务器负载均衡是否相同 if (balancer1 == balancer2 && balancer2 == balancer3 && balancer3 == balancer4){ System.out.println("服务器负载均衡具有唯一性"); } //增加服务器 balancer1.addServer("server1"); balancer1.addServer("server2"); balancer1.addServer("server3"); balancer1.addServer("server4"); for (int i = 0 ; i < 10;i++){ String server = balancer1.getServer(); System.out.println("分发请求至服务器:" + server); } }
虽然创建了四个对象
但是他们实际上是一同一个对象
通过单例模式可以确保LoadBalancer对象的唯一性
饿汉式
是单例模类实现起来最简单的单例类
结构图:
class EagerSingleton { private static final EagerSingleton instance = new EagerSingleton();
private EagerSingleton() { }
public static EagerSingleton getInstance() { return instance; }
}
当类被加载时,静态变量instance会被加载初始化
此时类的私有构造器会被调用
单例类的唯一实例将被创建
如果使用饿汉式来实现负载均衡LoadBalancer类的设计
则不会出现创建多个单例对象的情况
可确保单例对象的唯一性
懒汉式与线程锁定
结构图;
懒汉式在第一次调用getInstance()方法时实例化
在类加载时并不自行进行实例化
这种技术又称为延迟加载技术
即需要的时候在加载实例
为了避免多个线程同时调用getInstance()方法
可以使用关键字synchronized
class LazySingleton { private static LazySingleton instance = null;
private LazySingleton() { }
synchronized public static LazySingleton getInstance() { if (instance == null) { instance = new LazySingleton(); } return instance; } }
懒汉式在getInstance()方法前面增加关键字synchronized进行线程锁
以处理多个线程同时访问的问题
虽然上面的代码解决了线程的安全性问题,但是每次调用getInstance()时都需要进行线程锁定判断
在多线程高并发访问环境中,将会导致系统性能大大降低。
此时可以进行代码的改进:
public static LazySingleton getInstance() { if (instance == null) { synchronized (LazySingleton.class) { instance = new LazySingleton(); } } return instance; }
此时的问题:
在某一个瞬间线程A和线程B都在调用getInstance()方法,此时的instance对象为null
均能通过instance==null的判断
由于实现synchronized加载机制
线程A进入了synchronized锁定的代码中执行实例创建代码
线程B处理排队等待状态,必须等待线程A执行完毕之后才能进入synchronized锁定代码
但是A执行完毕之后,线程B并不知道实例已经创建
将继续创建新的实例,导致产生多个单例对象
违背了单例模式的设计思想
在进行代码改进:
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虚拟机锁做的一些代码优化
可能导致系统运行效率降低
因此使用双重检查锁定来实现单例模式也不是一种完美的实现方式
饿汉式单例类与懒汉式单例类比较
饿汉式:
优点:
1、于无须考虑多线程访问问题,可以确保实例的唯一性
2、从调用速度和反应时间角度来讲,由于单例对象一开始就得以创建,因此要优于懒汉式单例
缺点:
无论系统在运行时是否需要使用该单例对象,由于在类加载时该
对象就需要创建,因此从资源利用效率角度来讲,饿汉式单例不及懒汉式单例,而且在系统
加载时由于需要创建饿汉式单例对象,加载时间可能会比较长。
懒汉式:
在第一次使用时创建,无须一直占用系统资源,实现了延迟加载,但是必须处
理好多个线程同时访问的问题,特别是当单例类作为资源控制器,在实例化时必然涉及资源
初始化,而资源初始化很有可能耗费大量时间,这意味着出现多线程同时首次引用此类的机
率变得较大,需要通过双重检查锁定等机制进行控制,这将导致系统性能受到一定影响。
对于以上方法中提供一个更好的实现(Initialization Demand Holder (IoDH))
饿汉式单例类不能实现延迟加载,不管将来用不用始终占据内存
懒汉式单例类线程安全控制烦琐,而且性能受影响
此时的方法是对以上两种方式的一种整合:
在该类内部类中创建单例对象
在将该单例兑现通过getInstance()方法返回给外部使用
class Singleton { private Singleton() { }
private static class HolderClass { private final static Singleton instance = new Singleton(); }
public static Singleton getInstance() { return HolderClass.instance; }
}
public static void main(String args[]) { Singleton s1, s2; s1 = Singleton.getInstance(); s2 = Singleton.getInstance(); System.out.println(s1==s2); }
由于静态单例对象没有作为Singleton的成员变量直接实例化,因此类加载时不会实例化Singleton,第一次
调用getInstance()时将加载内部类HolderClass,在该内部类中定义了一个static类型的变量
instance,此时会首先初始化这个成员变量,由Java虚拟机来保证其线程安全性,确保该成员
变量只能初始化一次。由于getInstance()方法没有任何线程锁定,因此其性能不会造成任何影响。
既可以实现延迟加载,又可以保证线程安全,不影响系统性能,不失为
一种最好的Java语言单例模式实现方式
优点:
(1) 单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严
格控制客户怎样以及何时访问它。
(2) 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销
毁的对象单例模式无疑可以提高系统的性能。
(3) 允许可变数目的实例。基于单例模式我们可以进行扩展,使用与单例控制相似的方法来获
得指定个数的对象实例,既节省系统资源,又解决了单例单例对象共享过多有损性能的问
题。
缺点:
(1) 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
(2) 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角
色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的
本身的功能融合到一起。
(3) 现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如
果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次
利用时又将重新实例化,这将导致共享的单例对象状态的丢失。
使用场景:
(1) 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器或资源管理器,或者
需要考虑资源消耗太大而只允许创建一个对象。
(2) 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途
径访问该实例。