一、什么是单例模式
单例模式是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例类的特殊类。通过单例模式可以保证系统中个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的控制并节约系统资源。如果希望在系统中某个类的对象只能存在一个,单例模式是最好的解决方案。
二、为什么要使用单例模式
对于系统中的某些类来说,只有一个实例很重要,例如,一个系统中可以存在多个打印任务,但是只能有一个正在工作的任务;一个系统只能有一个窗口管理器或文件系统; 一个系统只能有一个计时工具或ID(序号)生成器。如在Windows中就只能打开一个任务管理器。如果不使用机制对窗口对象进行唯一化,将弹出多个窗口, 如果这些窗口显示的内容完全一致,则是重复对象,浪费内存资源;如果这些窗口显示的内容不一致,则意味着在某一瞬间系统有多个状态,与实际不符,也会给用户带来误解,不知道哪一个才是真实的状态。因此有时确保系统中某个对象的唯一性即一个类只能有一个实例非常重要。如何保证一个类只有一个实例并且这个实例易于被访问呢?定义一个全局变量可以确保对象随时都可以被访问,但不能防止我们实例化多个对象。一个更好的解决办法是让类自身负责保存它的唯一实例。这个类可以保证没有其他实例被创建,并且它可以提供一个访问该实例的方法。这就是单例模式的模式动机。
三、常见的单例模式
1、有频繁实例化然后销毁的情况,也就是频繁的 new 对象,可以考虑单例模式
2、创建对象时耗时过多或者耗资源过多,但又经常用到的对象
3、频繁访问 IO 资源的对象,例如数据库连接池或访问本地文件
举例:
1.网站在线人数统计,其实就是全局计数器,也就是说所有用户在相同的时刻获取到的在线人数数量都是一致的。要实现这个需求,计数器就要全局唯一,也就正好可以用单例模式来实现。
当然这里不包括分布式场景,因为计数是存在内存中的,并且还要保证线程安全。
public class Counter {
private static class CounterHolder{
private static final Counter counter = new Counter();
}
private Counter(){
System.out.println("init...");
}
public static final Counter getInstance(){
return CounterHolder.counter;
}
private AtomicLong online = new AtomicLong();
public long getOnline(){
return online.get();
}
public long add(){
return online.incrementAndGet();
}
}
2.配置文件访问类,项目中经常需要一些环境相关的配置文件,比如短信通知相关的、邮件相关的。比如 properties 文件,这里就以读取一个properties 文件配置为例,如果你使用的 Spring ,可以用 @PropertySource 注解实现,默认就是单例模式。如果不用单例的话,每次都要 new 对象,每次都要重新读一遍配置文件,很影响性能,如果用单例模式,则只需要读取一遍就好了。
public class SingleProperty {
private static Properties prop;
private static class SinglePropertyHolder{
private static final SingleProperty singleProperty = new SingleProperty();
}
/**
* config.properties 内容是 test.name=kite
*/
private SingleProperty(){
System.out.println("构造函数执行");
prop = new Properties();
InputStream stream = SingleProperty.class.getClassLoader()
.getResourceAsStream("config.properties");
try {
prop.load(new InputStreamReader(stream, "utf-8"));
} catch (IOException e) {
e.printStackTrace();
}
}
public static SingleProperty getInstance(){
return SinglePropertyHolder.singleProperty;
}
public String getName(){
return prop.get("test.name").toString();
}
public static void main(String[] args){
SingleProperty singleProperty = SingleProperty.getInstance();
System.out.println(singleProperty.getName());
}
}
3.数据库连接池的实现,也包括线程池。为什么要做池化,是因为新建连接很耗时,如果每次新任务来了,都新建连接,那对性能的影响实在太大。所以一般的做法是在一个应用内维护一个连接池,这样当任务进来时,如果有空闲连接,可以直接拿来用,省去了初始化的开销。所以用单例模式,正好可以实现一个应用内只有一个线程池的存在,所有需要连接的任务,都要从这个连接池来获取连接。如果不使用单例,那么应用内就会出现多个连接池,那也就没什么意义了。如果你使用 Spring 的话,并集成了例如 druid 或者 c3p0 ,这些成熟开源的数据库连接池,一般也都是默认以单例模式实现的。
四,单例模式的四种实现方式
package com.pats.file.design.Single;
public class Singleton {
// 1、饿汉式(线程安全,调用效率高,但是不能延时加载),不推荐
// private static Singleton instance = new Singleton();
// private Singleton() {};
// public static Singleton getInstance() {
// return instance;
// }
//2、懒汉式懒汉模式并没有考虑线程安全问题,在多个线程可能会并发调用它的getInstance()方法,导致创建多个实例
// private static Singleton instance = null;
// private Singleton() {};
// public static synchronized Singleton getInstance(){
// if(instance==null){
// instance=new Singleton();
// }
// return instance;
// }
//3.Double CheckLock实现单例:DCL也就是双重锁判断机制加锁的懒汉模式,看起来即解决了线程并发问题,又实现了延迟加载,然而它存在着性能问题,依然不够完美。
// public static volatile Singleton instance = null;
// private Singleton(){};
// public static Singleton getInstance() {
// if(instance == null) {
// synchronized (Singleton.class) {
// if(instance == null) {
// instance = new Singleton();
// }
// }
//
// }
// return instance;
// }
//4.通过静态内部类来实现
// public static class SingletonHandler{
//
// public static Singleton instance = new Singleton();
//
// }
// private Singleton() {};
// public static Singleton getInstance() {
// return SingletonHandler.instance;
// }
//5.最后一种实现方式:枚举。
// 枚举元素本身就是单例
// INSTANCE;
//
// //添加自己需要的操作
// public void singletonOperation(){
// }
}
上面提到的四种实现单例的方式都有共同的缺点:
1)需要额外的工作来实现序列化,否则每次反序列化一个序列化的对象时都会创建一个新的实例。
2)可以使用反射强行调用私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。
而枚举类很好的解决了这两个问题,使用枚举除了线程安全和防止反射调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。因此,《Effective Java》作者推荐使用的方法。不过,在实际工作中,很少看见有人这么写。
总结
本文总结了五种Java中实现单例的方法,其中前两种都不够完美,双重校验锁和静态内部类的方式可以解决大部分问题,平时工作中使用的最多的也是这两种方式。枚举方式虽然很完美的解决了各种问题,但是这种写法多少让人感觉有些生疏。个人的建议是,在没有特殊需求的情况下,使用第三种和第四种方式实现单例模式。
参考:https://blog.csdn.net/zxl646801924/article/details/86999584