单例模式
描述:单例(Singleton)模式的定义:指一个类只有一个实例,且该类能自行创建这个实例的一种模式。
核心特点
- 单例类只有一个实例对象;
- 该单例对象必须由单例类自行创建;
- 单例类对外提供一个访问该单例的全局访问点。
实现方式
通常,普通类的构造函数是公有的,外部类可以通过“new 构造函数()”来生成多个实例。但是,如果将类的构造函数设为私有的,外部类就无法调用该构造函数,也就无法生成多个实例。这时该类自身必须定义一个静态私有实例,并向外提供一个静态的公有函数用于创建或获取该静态私有实例。
角色功能
- 单例类:包含一个实例且能自行创建这个实例的类。
- 访问类:使用单例的类。
类图:
优点:
- 单例模式可以保证内存里只有一个实例,避免重复创建和销毁,减少了内存的开销。
- 可以避免对资源的多重占用。
- 单例模式设置全局访问点,可以优化和共享资源的访问。
缺点:
- 单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则。
- 在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。
- 单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。
实现代码:
实现要点:private修饰默认构造函数、private static修饰实例对象、public static修饰获取实例的方法
懒汉式单例
该模式的特点是类加载时没有生成单例,只有当第一次调用 getlnstance 方法时才去创建这个单例,第一次用到才创建实例,所以叫懒汉(这其实是优点)。
特点:
- 实现简单
- 首次使用时才创建对象实例(实现了懒加载)
- 若想实现线程安全则必须使用synchronized,效率较低
public class LazySingleton {
private static volatile LazySingleton instance = null; //保证 instance 在所有线程中同步
private LazySingleton() {
} //private 避免类在外部被实例化
public static synchronized LazySingleton getInstance() {
//getInstance 方法前加同步
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
双重校验机制
属于懒汉式的拓展实现
特点:
- 能实现懒加载,也是线程安全的,且效率较高
- 实现复杂,需要两次校验(进入同步块后还要校验一次)
public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
问:为什么synchronized语句不把第一个if判断语句要包括进去?
答:包括进去就和synchronized方法没区别了,效率很低。
问:为什么synchronized语句块创建实例对象前还要再判断一次实例是否被创建?
答:为了保证实例只被创建一次。如果初始实例为null时,有多个线程进入到第一个判断语句并竞争锁,拿到锁的线程如果已经创建了实例后,后续拿到锁的线程就不能再创建了,所以要再判断一次。换句话说synchronized去修饰实例创建语句时候,只能保证同时刻只有一个在创建,而不能保证后续没有别的线程去创建。
饿汉式单例
该模式的特点是类一旦加载就创建一个单例,保证在调用 getInstance 方法之前单例已经存在了,它是线程安全的,可以直接用于多线程而不会出现问题。
特点:
- 类加载时就创建好对象实例,也就是不管是否用到都初始化,浪费内存
- 天然的线程安全,不需要使用到锁
public class HungrySingleton {
//类加载时就会创建实例化对象
private static HungrySingleton instance = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return instance;
}
}
静态内部类
属于饿汉式的拓展,解决饿汉式无法实现懒加载问题,能实现和双重校验机制一样的效果。
在单例类里面创建一个SingletonHolder内部静态类,外部的单例类被加载时不会立马初始化内部静态类,只有去调用内部静态类的静态成员或静态方法才会触发类的加载,也能够实现第一次用到时才加载,也就是懒加载。
public class Singleton {
//内部静态类,单例类加载时不会立马初始化,只有访问内部静态变量时才初始化
private static class SingletonHolder {
private static Singleton INSTANCE = new Singleton();
}
//空的构造方法
private Singleton (){
}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
枚举式
单例模式的最佳方法,非常简洁、高效、线程安全、支持序列化机制、无法被反射机制破解(其它方式都可以)。
public enum Singleton {
INSTANCE; // 枚举里的属性相当于Singleton的实例
private Person instance;
Singleton() {// 私有构造函数 默认是 private
instance = new Person(); // 在构造函数中完成实例化操作
}
public static Person getInstance() {// 提供公有方法对其访问
return instance;
}
}
// 在外部使用Singleton.INSTANCE.getInstance();来调用
}
如何选?
- 懒汉式要么线程不安全,要么效率低,要么实现复杂,不建议使用。
- 如果单例对象占用资源大,需要懒加载,建议用内部静态类方式(优于懒汉式)。
- 如果单例对象占用资源小,不需要懒加载,建议用枚举式(优于饿汉式)。
应用场景
对于 Java来说,单例模式可以保证在一个 JVM 中只存在单一实例。单例模式的应用场景主要有以下几个方面。
- 某类需要频繁创建销毁,且资源消耗又比较大的对象,如线程池、数据库连接池、配置文件、日志管理等,单例可提高性能和节省资源。
- 当对象需要多线程共享访问的场合。由于单例模式只允许创建一个对象,能够保证线程安全性且达到共享目的。
- 外部和内部资源管理。对于管理外部打印机、回收站、内部属性文件的系统,单例模式可以确保这些资源被系统中的唯一实例所管理。
- 对于一些控制硬件级别的操作,或者从系统上来讲应当是单一控制逻辑的操作,如果有多个实例,则系统会完全乱套。