第8章 单例模式
8.1 单例模式概述
保证一个类只有一个实例并且这个实例易于被访问。
- 定义统一的全局变量确保对象随时被访问,
但不能防止创建多个对象。 - 类自身负责和保存唯一实例,保证不能创建其他实例且提供访问实例的方法。
单例模式(Singleton Pattern):确保一个类只有一个实例,并提供一个全局访问点来访问这个唯一实例。
8.2 单例模式结构与实现
8.2.1 单例模式结构
8.2.2 单例模式实现
package designpatterns.singleton;
public class Singleton {
private static Singleton instance = null;
private Singleton() {
}
public static Singleton getInstance() {
if (instance == null)
instance = new Singleton();
return instance;
}
}
package designpatterns.singleton;
public class Client {
public static void main(String[] args) {
Singleton s1=Singleton.getInstance();
Singleton s2=Singleton.getInstance();
if(s1==s2){
System.out.println("两个对象是相同实例.");
}else {
System.out.println("两个对象是不同实例");
}
}
}
单例模式:
- 单例类构造函数可见性为private
- 提供一个类型为自身的静态私有成员变量
- 提供一个公有的静态工厂方法
8.3 单例模式应用实例
实例说明
某软件公司承接了一个服务器负载均衡(Load Balance)软件的开发工作,该软件运行在一台负载均衡服务器上,可以将并发访问和数据流量分发到服务器集群中的多台设备上进行并发处理,提高系统的整体处理能力,缩短响应时间。由于集群中的服务器需要动态删减,且客户端请求需要统一分发﹐因此需要确保负载均衡器的唯一性,只能有一个负载均衡器来负责服务器的管理和请求的分发,否则将会带来服务器状态的不一致以及请求分配冲突等问题。如何确保负载均衡器的唯一性是该软件成功的关键,试使用单例模式设计服务器负载均衡器。
实例类图
- 单例类
- LoadBalancer
LoadBalancer:负载均衡器类,充当单例角色
package designpatterns.singleton;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class LoadBalancer {
private static LoadBalancer instancee = null;
private List serverList = null;
private LoadBalancer() {
serverList = new ArrayList();
}
public static LoadBalancer getLoadBalancer() {
if (instancee == null) {
instancee = new LoadBalancer();
}
return instancee;
}
public void addServer(String server) {
serverList.add(server);
}
public void removeServer(String server) {
serverList.remove(server);
}
public String getServer() {
Random random = new Random();
int i = random.nextInt(serverList.size());
return (String) serverList.get(i);
}
}
Client:
package designpatterns.singleton;
public class Client {
public static void main(String[] args) {
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);
}
}
}
运行结果及分析
饿汉式单例与懒汉式单例
饿汉式单例类
public class EagerSingleton {
private static final EagerSingleton instance = new EagerSingleton();
private EagerSingleton() {
}
public static EagerSingleton getInstance() {
return instance;
}
}
懒汉式单例类
public class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton() {
}
//使用synchronized关键字对方法加锁,确保任意时刻只有一个线程可执行该方法
synchronized public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
在上述懒汉式单例类,在getInstance()方法前面增加了关键字synchronized进行线程锁定,以处理多个线程同时访问的问题。
上述代码虽然解决了线程安全问题,但是每次调用getInstance()时都需要进行线程锁定判断,在多线程高并发访问环境中将会导致系统性能大大降低。
因此可以继续对懒汉式单例进行改进:
public class LazySingleton {
private static LazySingleton instance = null;
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (instance == null) {
//使用synchronized关键字对实例对象加锁,确保任意时刻只有一个线程生成单个实例对象
synchronized (LazySingleton.class) {
instance = new LazySingleton();
}
}
return instance;
}
}
假如在某一瞬间线程A和线程B都在调用getInstance()方法,此时instance对象为null值,均能通过"instance==null"的判断。由于实现了synchronized加锁机制,线程A进入synchronized锁定的代码中执行实例创建代码,线程B处于排队等待状态,必须等待线程A执行完毕后才可以进入synchronized锁定的代码。但当A执行完毕时线程B并不知道实例已经创建,将继续创建新的实例,导致产生多个单例对象,违背了单例模式的设计思想。
因此可以继续对懒汉式单例进行进一步改进:
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;
}
}
- 增加了volatile关键字确保多个线程的正确处理,但是也会屏蔽Java虚拟机所做的代码优化。
饿汉式单例类与懒汉式单例类比较
饿汉式单例类 | 懒汉式单例类 | 说明 | |
---|---|---|---|
调用速度快慢 | √ | 饿汉式单例对象随着对象实例化而创建;懒汉式单例对象在使用时对象实例静态方法创建 | |
反应时间快慢 | √ | 同上 | |
资源利用效率大小 | √ | 无论是否使用,饿汉式都已经初始化单例对象;懒汉式在需要的时候调用静态方法初始化单例对象 | |
加载时间快慢 | √ | 同上 | |
多线程访问单例唯一功能 | √ | √ | |
延迟加载功能 | × | √ | |
资源控制功能 | √ | 多线程首次引用几率变大,懒汉式机制控制对系统性能要求更高 |
使用静态内部类实现单例模式
//Initialization on Demand Holder (IoDH)
public 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->HolderClass->instance,初始化一次
- 无线程锁定,性能不影响
8.5 单例模式优缺点
8.5.1 单例模式优点
- 单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。
- 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。
- 允许可变数目的实例。基于单例模式可以进行扩展,使用与控制单例对象相似的方法来获得指定个数的实例对象,既节省系统资源,又解决了由于单例对象共享过多有损性能的问题。(注:自行提供指定数目实例对象的类可称为多例类。)
8.5.2 单例模式缺点
- 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
- 单例类的职责过重,在一定程度上违背了单一职责原则。因为单例类既提供了业务方法,又提供了创建对象的方法(工厂方法),将对象的创建和对象本身的功能耦合在一起。
- 现在很多面向对象语言(如Java,C#)的运行环境都提供了自动垃圾回收技术,因此如果实例化的共享对象长时间不被利用,系统会认为它是垃圾。会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。
8.5.3 单例模式适用环境
- 系统只需要一个实例对象。例如系统要求提供一个唯一的序列号生成器或资源管理器,或者因为资源消耗太大而只允许创建一个对象。
- 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。