单例模式
一、定义
单例模式:一个类在其生命周期内只有一个实例,该类能自行创建这个实例,所有获取该类实例的方法只能返回该实例,这种类设计的模式称为单例模式。
二、使用场景
单例模式使用条件:
1)有频繁实例化然后销毁的情况,也就是频繁的 new 对象,可以考虑单例模式;
2)创建对象时耗时过多或者耗资源过多,但又经常用到的对象;
3)频繁访问 IO 资源的对象,例如数据库连接池或访问本地文件;
单例模式使用场景:
1)Windows资源管理器,在Windows中只能打开一个资源管理器
2)Windows回收站,在Windows中只能打开一个回收站
3)网站计数器,所有用户在相同的时刻获取到的在线人数数量都是一致的,要实现这个需求,计数器就要全局唯一,也就正好可以用单例模式来实现。
4)应用程序的日志应用,一般都使用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
5)Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。Spring中@PropertySource 注解实现,默认就是单例模式。
6)数据库连接池的设计一般使用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因此使用单例模式来维护,就可以大大降低这种损耗。
7)多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。
8)操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。
三、单例模式实现方式
1)饿汉式(不推荐)
在类加载阶段就完成实例的初始化,通过类加载机制来保证线程安全。
程序中如果存在过多的饿汉式单例模式的实现,在系统启动时,会导致启动变慢。
public class HungrySingletonTest {
public static void main(String[] args) {
HungrySingleton hungrySingleton = HungrySingleton.getInstance();
System.out.println(hungrySingleton);
}
}
/**
-
通过类加载机制来完成实例的初始化,加载的时候就初始化,不存在延迟加载
*/
class HungrySingleton {
private static HungrySingleton instance = new HungrySingleton();public static HungrySingleton getInstance() {
return instance;
}private HungrySingleton(){
if (instance != null) {
throw new RuntimeException(“singleton not allow more than one instance”); // 防止反射攻击
}
}
}
2)懒汉式(可使用)
使用对象的时候加载,避免因对象加载过多引起系统启动慢,但容易出现线程安全问题。
package com.designpatterns.singleton;
/**
-
spring中ReactiveAdapterRegistry就是使用懒加载方式获取的单例对象
*/
public class LazySingletonTest {
public static void main(String[] args) {
// LazySingleton lazySingleton = LazySingleton.getInstance();
// System.out.println(lazySingleton);
// LazySingleton lazySingleton2 = LazySingleton.getInstance();
// System.out.println(lazySingleton2);new Thread(new Runnable() { @Override public void run() { LazySingleton lazySingleton = LazySingleton.getInstance(); System.out.println(lazySingleton); } }).start(); new Thread(new Runnable() { @Override public void run() { LazySingleton lazySingleton = LazySingleton.getInstance(); System.out.println(lazySingleton); } }).start();
}
}
/**
-
一个jvm只有一个实例
*/
class LazySingleton {private static volatile LazySingleton instance; // volatile关键字修饰的变量执行的内存空间,操作时会防止指令重排序
private LazySingleton(){
if (instance != null) {
throw new RuntimeException(“singleton not allow more than one instance”); // 防止反射攻击
}
}/**
- 方法上使用synchronized是可以完成单例的,但是此处使用synchronized太重,影响性能,且没有必要
- @return
/
public static LazySingleton getInstance() {
if (instance == null) {
synchronized (LazySingleton.class) {
if (instance == null) { // 双重校验
// 存在指令重排序 JIT、编译器、CPU
/*
new 一个对象时,原子操作有三步,1、分配空间->返回一个指向该空间的内存引用2、对空间进行初始化3、把内存引用赋值给引用变量
其中2、3两步存在指令重排序的问题,如果两个线程进入到单例获取方法,其中一个锁住类后,先做了赋值操作,后做初始化,另一个线程
判断对象是否存在时,由于第一个线程已经给变量赋值,所以第二个线程就获得一个没有初始化的对象,此时使用时会报空指针问题。故需要
加上一个关键字volatile
注意点:1)线程安全 2)防止指令重排序 3)双重检查优化
*/
instance = new LazySingleton(); // 该行操作不是原子的
// 1、分配空间,返回空间引用2、初始化3、引用赋值给变量
}
}
}
return instance;
}
}
3)枚举
使用枚举实现单例不存在安全问题,可自动防御反射攻击
推荐使用:线程安全、懒加载、性能好
public enum EnumSingleton {
INSTANCE;
public void test(){
System.out.println("hello singleton");
}
}
public class MainTest {
public static void main(String[] args) {
EnumSingleton enumSingleton = EnumSingleton.INSTANCE;
enumSingleton.test();
}
}
4)静态内部类
推荐使用:线程安全、懒加载、性能好
public class InnerClassSingletonTest {
public static void main(String[] args) {
InnerClassSingleton innerClassSingleton = InnerClassSingleton.getInstance();
System.out.println(innerClassSingleton);
}
}
class InnerClassSingleton{
static class InnerClass{
private static InnerClassSingleton instance = new InnerClassSingleton();
}
public static InnerClassSingleton getInstance() {
return InnerClass.instance;
}
private InnerClassSingleton(){
if (InnerClass.instance != null) {
throw new RuntimeException("singleton not allow more than one instance"); // 防止反射攻击
}
}
}
单例模式尽量不要实现序列化接口,如果单例类实现了序列化接口Serializable, 就可以通过反序列化破坏单例,所以我们可以不实现序列化接口,如果非得实现序列化接口,可以重写反序列化方法readResolve(), 反序列化时直接返回相关单例对象。
除枚举外,其他单例模式都需要防止反射攻击