单例模式(Singleton)
1. 定义&特点
指一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。例如,Windows 中只能打开一个任务管理器,这样可以避免因打开多个任务管理器窗口而造成内存资源的浪费,或出现各个窗口显示内容的不一致等错误。总之,选择单例模式就是为了避免不一致状态,避免政出多头。
单例模式特点:
- 单例类只有一个实例对象
- 该单例对象必须由单例类自行创建:把类的构造方法私有化,内部进行实例化,不让外部调用构造方法实例化
- 单例类对外提供一个访问该单例的全局访问点:提供一静态方法,返回实例对象
2. 单例模式的结构与实现
2.1. 结构
单例模式的主要角色:
- 单例类:包含一个实例且能自行创建这个实例的类。
- 访问类:使用单例的类。
2.2.实现
2.2.1.枚举方式(推荐使用)
特点:
- 可以处理线程同步问题
- 反序列化重复创建对象的问题
- 代码简洁
- Java作者也推荐使用
大佬对枚举实现单例模式的详解
public class TestEnum{
public static void main(String[] args){
Singleton instance = Singleton.INSTANCE;
Singleton instance2 = Singleton.INSTANCE;
//检验创建的是同一个实例
System.out.println(instance ==instance2 );
//enum也可以调用方法
System.out.println(instance.show);
}
}
enum Singleton{
INSTANCE;
public void show(){
System.out.println("enum 可以调用show方法")
}
}
2.2.2 静态内部类(推荐使用)
原理:
根据 静态内部类 的特性,同时解决了按需加载、线程安全的问题,同时实现简洁
- 在静态内部类里创建单例,在装载该内部类时才会去创建单例
- 线程安全:类是由 JVM加载,而JVM只会加载1遍静态类,保证只有1个单例
class Singleton {
// 1. 创建静态内部类
private static class NewSingleton {
// 在静态内部类里创建单例
private static Singleton ourInstance = new Singleton();
}
// 私有构造函数
private Singleton() {
}
// 延迟加载、按需创建
public static Singleton getInstance() {
return NewSingleton.ourInstance;
}
}
调用过程说明:
- 外部调用类的getInstance()
- 自动调用NewSingleton.ourInstance
2.1 此时单例类NewSingleton得到初始化
2.2 而该类在装载 & 被初始化时,会初始化它的静态域,从而创建单例;
2.3 由于是静态域,因此只会JVM只会加载1遍,Java虚拟机保证了线程安全性 - 最终只创建1个单例
2.2.3.饿汉式(耗资源,不太推荐)
该模式的特点是类一旦加载就创建一个单例,保证在调用 getInstance 方法之前单例已经存在了。
public class HungrySingleton{
private static final HungrySingleton ourInstance=new HungrySingleton();
private HungrySingleton(){}
public static HungrySingleton getInstance(){
return ourInstance;
}
}
2.2.4.懒汉式(线程不安全)
该模式的特点是类加载时没有生成单例,只有当第一次调用 getlnstance 方法时才去创建这个单例
public class LazySingleton{
//1. 类加载时,先不自动创建单例:将单例的引用先赋值为 Null
//保证 instance 在所有线程中同步
private static volatile LazySingleton ourInstance=null;
// 2. 构造函数 设置为私有权限,禁止他人创建实例
private LazySingleton(){}
// 3. 加入同步锁
public static synchronized LazySingleton getInstance(){
if(ourInstance==null){
ourInstance=new LazySingleton();
}
return ourInstance;
}
}
双重校验锁(推荐使用)
针对同步锁的缺点:每次访问都要进行线程同步(即 调用synchronized锁),造成过多的同步开销(加锁 = 耗时、耗能)
实际上只需在第1次调用该方法时才需要同步,一旦单例创建成功后,就没必要进行同步
(2.1)原理
在同步锁的基础上,添加1层 if判断:若单例已创建,则不需再执行加锁操作就可获取实例,从而提高性能。
(2.2)具体实现
class Singleton {
private static Singleton ourInstance = null;
private Singleton() {
}
public static Singleton newInstance() {
// 加入双重校验锁
// 校验锁1:第1个if
if( ourInstance == null){ // ①
synchronized (Singleton.class){ // ②
// 校验锁2:第2个if,防止在第一个if和第二个if执行期间,有其他线程实例化Singleton
if( ourInstance == null){
ourInstance = new Singleton();
}
}
}
return ourInstance;
}
}
// 说明
// 校验锁1:第1个if
// 作用:若单例已创建,则直接返回已创建的单例,无需再执行加锁操作
// 即直接跳到执行 return ourInstance
// 校验锁2:第2个 if
// 作用:防止多次创建单例问题
// 原理
// 1. 线程A调用newInstance(),当运行到②位置时,此时线程B也调用了newInstance()
// 2. 因线程A并没有执行instance = new Singleton();,此时instance仍为空,因此线程B能突破第1层 if 判断,运行到①位置等待synchronized中的A线程执行完毕
// 3. 当线程A释放同步锁时,单例已创建,即instance已非空
// 4. 此时线程B 从①开始执行到位置②。此时第2层 if 判断 = 为空(单例已创建),因此也不会创建多余的实例
2.2.5.懒汉式和饿汉式比较
3.单例模式优缺点
优点
- 提供了对唯一实例的受控访问;
- 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能;
- 可以根据实际情况需要,在单例模式的基础上扩展做出双例模式,多例模式;
缺点
- 单例类的职责过重,里面的代码可能会过于复杂,在一定程度上违背了“单一职责原则”。
- 如果实例化的对象长时间不被利用,会被系统认为是垃圾而被回收,这将导致对象状态的丢失。