单例模式号称是最简单的设计模式,但是通过对单例模式一些学习,发现里面知识点挺多的,涉及的一些内容并不是初学者常见的知识。
本文目录结构如下:
- 单例模式都有哪些写法
- 单例模式需要注意的地方
- 单例模式使用场景
- 单例模式和静态方法的区别
单例模式写法:
1-饿汉式单例
/**
* 饿汉式单例,非懒加载
*
* 以空间换时间
*
* 线程安全
*/
public class EagerSingleton {
// 注意:类被加载的时候初始化实例
private static EagerSingleton instance = new EagerSingleton();
// 注意:private类型的构造函数,保证其他类对象不能直接new一个该对象的实例
private EagerSingleton() {
}
public static EagerSingleton getInstance() {
return instance;
}
}
2-懒汉式单例
/**
* 懒汉式单例,实现了懒加载
*
* 线程安全
*/
public class LazySingleton {
private static LazySingleton instance = null;
// 注意:private类型的构造函数,保证其他类对象不能直接new一个该对象的实例
private LazySingleton() {
}
/**
* 注意synchronized
*/
public static synchronized LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
3-双重检查
public class DoubleCheckSingleton {
// 注意:这里必须加volatile,否则其他线程可能因为线程缓存,再次判断到实例为null
private volatile static DoubleCheckSingleton instance = null;
// 注意:private类型的构造函数,保证其他类对象不能直接new一个该对象的实例
private DoubleCheckSingleton() {
}
/**
* 优点: <br/>
* 1、先检查实例是否存在,如果存在,直接返回,不用线程同步<br/>
* 2、如果实例不存在,再进入同步代码块,同时再判断一下是否已经存在,如果不存在,则确实不存在,可以在当前线程中初始化<br/>
* 3、这里instance必须结合volatile关键字使用,否则其他线程可能因为线程缓存,再次判断到实例为null
*/
public static DoubleCheckSingleton getInstance() {
if (instance == null) {
synchronized (DoubleCheckSingleton.class) {
if (instance == null) {
instance = new DoubleCheckSingleton();
}
}
}
return instance;
}
}
4-静态内部类实现的单例
/**
* 线程安全
*
* 懒加载
*/
public class HolderSingleton {
private HolderSingleton() {
}
// 内部类前面加static,表示类级内部类
// 类级内部类特点:使用时才会被加载
public static class InstanceHolder {
// 类/静态变量的初始化是由JVM保证线程安全的
private static HolderSingleton instance = new HolderSingleton();
}
/**
* 当getInstance方法第一次被调用的时候,它第一次读取SingletonHolder.instance,导致SingletonHolder类得到初始化;
* 而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建Singleton的实例;
* 由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。
* 这个模式的优势在于,getInstance方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本。
*/
public static HolderSingleton getInstance() {
return InstanceHolder.instance;
}
}
5-枚举实现的单例
/**
* 1、枚举本身构造方法是private,也就是枚举本身就是单例的
* 2、枚举是线程安全的
* 3、枚举实现了序列化,其他几种方式序列化都有问题
*/
public enum EnumSingleton {
instance;
}
以上几种写法,最佳写法是第五种,利用枚举实现。
下面有一种不太常见,在各种教材和资料中不太会出现的写法:单例注册表
6-单例注册表实现的单例
package spring;
import java.util.HashMap;
public class SingletonWW {
private static HashMap registry = new HashMap();
public SingletonWW(){}
public synchronized static SingletonWW getInstance(String name){
if(name != null) {
if(registry.get(name) == null) {
try {
registry.put(name, Class.forName(name).newInstance());
} catch(Exception ex) {
ex.printStackTrace();
}
} else {
return (SingletonWW) registry.get(name);
}
}
return null;
}
}
单例模式需要注意的地方:
写单例模式,有几个点需要注意:
- 构造方法必须是private类型,保证其他类对象不能直接new一个该对象的实例。
- 要考虑到线程安全问题,高并发的问题。
- 考虑懒加载的问题。
单例模式使用场景:
单例模式只允许创建一个对象,因此节省内存,加快对象访问速度,因此对象需要被公用的场合适合使用,如多个模块使用同一个数据源连接对象等等。如:
- 需要频繁实例化然后销毁的对象。
- 创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
- 有状态的工具类对象。
- 频繁访问数据库或文件的对象。
以下都是单例模式的经典使用场景:
- 资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如上述中的日志文件,应用配置。
- 控制资源的情况下,方便资源之间的互相通信。如线程池等。
应用场景举例:
- 外部资源:每台计算机有若干个打印机,但只能有一个PrinterSpooler,以避免两个打印作业同时输出到打印机。内部资源:大多数软件都有一个(或多个)属性文件存放系统配置,这样的系统应该有一个对象管理这些属性文件 。
- Windows的Task Manager(任务管理器)就是很典型的单例模式(这个很熟悉吧),想想看,是不是呢,你能打开两个windows task manager吗? 不信你自己试试看哦~
- windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
- 网站的计数器,一般也是采用单例模式实现,否则难以同步。
- 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
- Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。
- 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。
- 多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。
- 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。
单例模式和“静态类”的区别
“静态类”指“所有方法皆为静态方法的类”
作为一个小白,个人感觉这俩确实太像了,有些场景应该都可以。网上有人列举出了一些区别,不过没有什么说服力,总结一下吧:
- 静态方法是面向过程的,而非面向对象的编程思想 。单例是对象(实例),而静态类是变量和方法的集合 。
- 单例可以被继承,这是一个很大的好处,这便于用户overwrite其中的某方法,当然,继承单例的场景较少见;而静态类一般不被继承。关于单例的继承细节,这里暂不讨论,有几种办法,有兴趣的同学可以自行阅读JDK的Calendar类。
- 如果希望在类加载的时候做复杂的操作,那么在静态类中,需要引入static块来初始化数据,如果期间抛出了异常,就可能发生一个“ClassDefNotFoundError”的诡异错误,这对问题定位是不利的。
参考列表: