目录
1.什么是单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
2.介绍
意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决:一个全局使用的类频繁地创建与销毁。
何时使用:当您想控制实例数目,节省系统资源的时候。
如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
关键代码:构造函数是私有的。
优点:
- 在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
- 避免对资源的多重占用(比如写文件操作)。
缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。
3.代码实现
饿汉式
主要利用类加载到内存后,就实例化一个单例,JVM保证线程安全。
简单实用,推荐实用!
唯一缺点:不管用到与否,类装载时就完成实例化,导致内存资源的消耗。
public class Singleton {
// 1. 将类的构造函数私有化,避免外界使用构造函数创建实例
private Singleton() { }
// 2. 定义一个私有的静态实例对象,并进行初始化
private static Singleton instance = new Singleton();
// 3. 提供一个公有的获取实例对象的静态方法
public static Singleton getInstance(){
return instance;
}
}
懒汉模式,线程不安全
虽然达到了延迟加载初始化的目的,但却带来线程不安全的问题。
public class Singleton {
// 1. 将类的构造函数私有化,避免外界使用构造函数创建实例
private Singleton() { }
// 2. 定义声明一个私有的静态实例对象,并没有初始化对象
private static Singleton instance;
// 3. 提供一个公有的获取实例对象的静态方法,在获取时来初始化实例对象
public static Singleton getInstance(){
// 懒汉式是在得到本类对象时,才会创建该对象
// 如果不进行判断,每次都会创建一个新的对象
// 所以加一个判断,判断 instance 是否为 null, 如果为null,就需要创建新的对象
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
懒汉式,第一种实现方式,在调用getInstance()方法获取时,Singleton类的对象的时候才开始初始化Singleton类的对象,这样就实现了懒加载
的效果,但是只能在单线程下使用;而如果在多线程情况下,就会出现线程安全问题。
如一个线程进入了if(instance == null )
判断语句,还未来得及往下执行初始化操作,另一个线程也通过了这个判断语句,这时便会产生多个实例,所以在多线程环境下不可使用这种方式。
懒汉模式,线程安全
虽然达到了线程安全的目的,但是在方法上加同步锁后,导致效率降低。
public class Singleton {
// 1. 将类的构造函数私有化,避免外界使用构造函数创建实例
private Singleton() { }
// 2. 定义声明一个私有的静态实例对象,并没有初始化对象
private static Singleton instance;
// 3. 提供一个公有的获取实例对象的静态方法,在获取时来初始化实例对象,并在方法上加上同步锁关键字synchronized。
public static synchronized Singleton getInstance(){
// 懒汉式是在得到本类对象时,才会创建该对象
// 如果不进行判断,每次都会创建一个新的对象
// 所以加一个判断,判断 instance 是否为 null, 如果为null,就需要创建新的对象
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
懒汉式,第二种实现方式,实现了懒加载的效果,同时也解决了多线程安全问题,但是在getInstance()方法上添加了synchronized关键字,导致该方法的执行效果特别低。
但是我们可以看出,每次获取实例都要进行同步(加锁),因此效率较低,并且可能很多同步都是没必要的。
双重检查机制
public class Singleton {
// 1. 将类的构造函数私有化,避免外界使用构造函数创建实例
private Singleton() { }
// 2. 定义声明一个私有的静态实例对象,并没有初始化对象
// 可能在jvm编译过程中,发生指令重排序问题,导致空指针异常,需要加 volatile关键字
private static volatile Singleton instance;
// 3. 提供一个公有的获取实例对象的静态方法,在获取时来初始化实例对象,并在初始化对象时加上同步代码块。
public static Singleton getInstance(){
// 懒汉式是在得到本类对象时,才会创建该对象
// 如果不进行判断,每次都会创建一个新的对象
// 第一次判断,判断 instance 是否为null, 如果为null,就需要创建新的对象,如果不为null,就不需要抢占锁,直接返回对象
if(instance == null) {
// 在此加上同步代码块
synchronized(Singleton.class){
// 这里加上第二次判断,避免重复创建新对象。
if(instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
指令重排问题:
在new对象时即 instance = new Singleton()
,正常它会发生这三个步骤:
- 在JVM的堆中申请一块内存空间
- 对象进行初始化操作
- 将堆中的内存空间的引用地址赋值给一个引用变量instance。
但是如果没有加上volatile关键字时,可能导致出现的步骤为 1 -> 3 -> 2
这时候就会出现空指针异常啦,所以我们得加在instance变量加上关键字 volatile
,确保在初始化变量时不会发送指令重排序问题。