设计模式学习笔记(六)单例模式
概念
在某些系统中,有的类我们只希望有一个实例,例如windows的任务管理器,无论启动多少次,只有一个窗口。回到我们的代码中,有时需要确保系统中某个类只有唯一一个实例,当这个唯一实例创建成功之后,我们无法再创建一个同类型的其他对象,所有的操作都只能基于这个唯一实例。为了确保对象的唯一性,我们可以通过单例模式来实现,这就是单例模式的动机所在。
单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式是一种对象创建型模式。
单例模式有三个要点:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。
示例
单例模式 任务管理器改造示例:
class TaskManager
{
public TaskManager() {……} //初始化窗口
public void displayProcesses() {……} //显示进程
public void displayServices() {……} //显示服务
……
}
第一步:为了隐藏这个类的构造函数不向外暴露,因此将构造函数改为私有
private TaskManager() {……} //初始化窗口
第二步:构造函数已经对外隐藏,无法从外部创建实例,我们可以通过类的内部创建
private static TaskManager tm = null;
第三步:此时用static来修饰TaskManager类的变量,保证了系统只能有一个实例,我们如何获取呢,使用共有的静态方法
public static TaskManager getInstance() {
if (tm == null) {
tm = new TaskManager(); //此方法只会第一次调用getInstance的时候执行
}
return tm;
}
单例模式结构图中只包含一个单例角色:
● Singleton(单例):在单例类的内部实现只生成一个实例,同时它提供一个静态的getInstance()工厂方法,让客户可以访问它的唯一实例;为了防止在外部对其实例化,将其构造函数设计为私有;在单例类内部定义了一个Singleton类型的静态对象,作为外部共享的唯一实例。
示例:负载均衡器的简单实现
在分布式部署的情况下,客户的多个请求需要分发到不同的服务器上处理,以保证相对够快的响应速度,整个系统的请求需要有一个统一的负载均衡器来实现,因此采用单例模式。
/**
* 使用单例模式简单实现负载均衡器,每个请求都通过这个负载均衡器执行,
* 为每个请求随机分发服务器serverList来处理这个请求
* */
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);
}
}
模拟客户端发送请求
//四个同实例的负载均衡器,模拟在系统中的多个模块中使用
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("Server 1");
balancer1.addServer("Server 2");
balancer2.addServer("Server 3");
balancer2.addServer("Server 4");
balancer3.addServer("Server 5");
balancer3.addServer("Server 6");
balancer4.addServer("Server 7");
balancer4.addServer("Server 8");
//模拟客户端请求的分发
for (int i = 0; i < 20; i++) {
String server = balancer1.getServer();
System.out.println("分发请求至服务器: " + server);
}
实例的唯一性
在上述的负载均衡器中,存在一个严重的问题,当多线程并发的情况下,用户A调用getLoadBalancer后,判断instance==null成功,再创建实例的过程中,instance还没有被创建完成,但是用户B也调用了getLoadBalancer,因此判断instance == null也成功,这就导致了出现了多个实例,违反了单例模式。
因此我们解决这个问题,就需要学习饿汉式单例类和懒汉式单例类
饿汉式单例类
饿汉式单例类的实现方法很简单,顾名思义,“饿汉”代表非常饿,上来就啥都吃啥都要,因此我们上来就创建实例。
class EagerSingleton {
public class LoadBalancer {
private static LoadBalancer instance = new LoadBalancer();
private LoadBalancer () { }
public static LoadBalancer getInstance() {
return instance;
}
}
此模式在工程启动时就会创建,因此只会出现一个实例。
懒汉式单例类
懒汉式单例类又称为延迟加载(Lazy Load)技术,也是顾名思义,“懒汉”出名的懒,因此只有用到他的时候,他才“活动一下”,只要调用getInstance的时候才会去创建。普通实现方式与任务管理器改造示例一样,因此我们需要针对多线程进行代码块的同步
关键字
synchronized用法详解
我们需要改造实例的生成方法
synchronized public static LoadBalancer getLoadBalancer() {
if (instance == null) {
instance = new LoadBalancer();
}
return instance;
}
这种方式是每个线程执行到此代码块的时候就先看一下是否有线程正在执行这个,如果有则其他线程都等待,知道上一个执行完成,再进行下一个线程的执行。此方式极大的影响性能。
为了减小性能的影响,修改代码如下:
public static LoadBalancer getLoadBalancer() {
if (instance == null) {
synchronized(LoadBalancer.class) {
instance = new LoadBalancer();
}
}
return instance;
}
此方法随之带来的问题是,只锁住了创建实例的代码,判断的方法并没有锁,因此还是会出现多个实例的情况。
我们可以引入双重检查锁定(Double-Check Locking)及修饰符volatile
private volatile static LoadBalancer instance = null;
//懒汉单例模式
public static LoadBalancer getLoadBalancer() {
if (instance == null) {
synchronized(LoadBalancer.class) {
if (instance == null) {
instance = new LoadBalancer();
}
}
}
return instance;
}
解释:在上述方法中,
1、用户A开始创建执行到了getLoadBalancer方法,并判断成功,开始进入锁的代码,此时LoadBalancer正在创建…
2、用户B同时也执行到了getLoadBalancer方法,并判断instance等于null,等待用户A线程的结束中…
3、此时A创建完成并赋值给instance,由于instance是被volatile修饰的变量,因此,instance在主内存中立即被刷新…
4、A线程执行完毕,B线程进入锁,此时主内存的instance已经不为空,则跳出直接return。
ps:volatile主要有两个功能,代码重排隔断,与强制刷写
1、代码重排是java的一种提高性能的机制,在代码的运行过程中会进行代码重排,但是当遇到volatile修饰的变量时,会被隔断;volatile修饰的变量前的代码进行重排,volatile修饰的变量后的代码重排,两批代码互不干涉。
2、在多线程的情况下,每个线程都有一份内存副本,保存着当前用到的变量,但如果遇到volatile修饰的变量被重新赋值,那么这个线程会将副本中的volatile修饰的变量的值直接强制刷写到主内存中。
总结:饿汉模式在启动时创建,会导致整个系统启动的时候很慢。
懒汉模式由于volatile关键字会屏蔽Java虚拟机所做的一些代码优化,可能会导致系统运行效率降低,因此即使使用双重检查锁定来实现单例模式也不是一种完美的实现方式。
同时解决饿汉与懒汉模式缺点的方法
我们可以利用静态内部类的特性
class LoadBalancer {
private LoadBalancer() {
}
private static class HolderClass {
private final static LoadBalancer instance = new LoadBalancer();
}
public static LoadBalancer getInstance() {
return HolderClass.instance;
}
public static void main(String args[]) {
LoadBalancer s1, s2;
s1 = LoadBalancer.getInstance();
s2 = LoadBalancer.getInstance();
System.out.println(s1==s2);
}
}
**代码解释:**静态内部类 HolderClass 包含一个静态变量 instance 作为唯一的实例。
1、在私有的构造方法 private LoadBalancer() 中,防止了外部通过 new 关键字来创建实例。
2、在静态内部类 HolderClass 中,定义了一个私有且 final 的静态变量 instance,它在静态内部类第一次被加载时进行实例化。由于静态变量只会初始化一次,因此该实例只会被创建一次。
3、在公共静态方法 getInstance() 中,通过访问静态内部类的静态变量 instance 来获取单例对象。**由于静态内部类的静态成员在第一次使用时才会被初始化,因此当第一次调用 getInstance() 方法时,会触发静态内部类的加载和静态变量的初始化。**而后续的调用则直接返回已经初始化好的静态变量 instance。
静态内部类解释:
静态内部类中的 static 变量只会初始化一次,是因为静态变量属于类级别的,而不是实例级别的。静态变量随着类的加载而被初始化,并且仅在类加载的过程中进行一次初始化。
当 JVM 加载包含静态内部类的外部类时,会先加载外部类,然后再加载内部类。在加载内部类时,静态变量会被初始化。由于静态变量的初始化是在类加载的过程中完成的,所以它只会发生一次。
即使创建多个静态内部类的实例,它们都会共享同一个静态变量的值。因为该静态变量是与类本身相关联的,而不是与实例相关联的。如果对其中一个实例修改了静态变量的值,那么其它实例也会受到影响,因为它们共享同一个静态变量。
需要注意的是,静态内部类中的非静态变量每个实例都有自己的副本,并且在创建实例时会随着实例的创建而初始化。
总结起来,静态内部类中的静态变量只会初始化一次,这是因为静态变量属于类级别的,与实例无关。在静态内部类的实现中,当多个线程同时访问 getInstance() 方法时,由于类加载机制的特性,只会有一个线程可以进入静态内部类进行实例化,其他线程会被阻塞等待。
总结
单例模式的主要优点:
1、确保只有一个实例:单例模式确保一个类只有一个实例对象存在,可以避免多次实例化相同对象,节省系统资源。
2、全局访问点:单例对象通常被设计为全局访问点,可以在程序的任何地方访问该实例,方便进行统一的管理和调用。
3、节省资源:由于单例模式只创建一个实例,可以减少内存占用和对象创建销毁的开销。
4、实现线程安全:通过适当的实现方式,单例模式可以保证在多线程环境下的线程安全性。
单例模式的主要缺点:
1、不透明性:单例模式隐藏了类的实例化细节,使代码的可读性变差,使用不当可能导致代码难以理解和维护。
2、难以扩展:因为单例模式只允许存在一个实例,所以扩展时需要小心处理,可能需要修改现有的代码。
3、单一职责原则:由于单例对象具有全局访问性,容易被滥用,导致一个类承担过多的职责,违背了单一职责原则。
适用场景:
1、系统中只需要一个实例对象,如配置文件、线程池、日志记录器等。
2、需要频繁实例化和销毁的对象,使用单例模式可以避免频繁创建和销毁的开销。
3、需要全局访问点,方便统一管理和调用的对象。
4、资源共享的情况下,可以节省系统资源。
5、需要注意的是,在使用单例模式时需要考虑线程安全性、并发访问控制以及对全局状态的影响,以确保正确使用和避免潜在的问题。