最简单的设计模式之一
单例模式主要用途:保证一个类只能有一个实例,并且在全局提供一个访问点。
常用实现方式:懒汉式、饿汉式、静态内部类式、枚举式
为什么需要单例模式?
假如此时你想读取一个XML配置文件,且该配置会被多次引用,那在每次引用时,就需要实例化一个Java Bean,但实际上我们全程只需要实例化一次,多处调用即可,当配置文件越来越多时,实例的创建占用系统资源是不可忽视的。那么此时就需要用到单例模式。
单例模式常用实现方式
要想保证一个类只能一个实例,那就不能让外面的代码随便访问构造方法创建实例,把创建实例的权限回收,让类自身负责自己的实例创建工作,并向外部提供一个访问这个实例的接口,因此构造方法必须私有化。
最初的最常见的两个实现方式:懒汉式和饿汉式
相同点
- 构造方法私有
- 都拥有提供外部获取实例的public方法getInstance()
- 因为该类不能多次实例化,所以获取实例的方法必须为静态方法,静态方法中的类必须为静态类,因此存储创建的类实例也必须是静态
不同点:
- 一个实例初始化了,一个没初始化
- 懒汉式存在线程安全问题
- 存储创建类实例的变量uniqueInstance设置为static一个主动一个被动
懒汉式:见文知意,比较懒,一上来没有实例化对象,而是在需要用到的时候才去实例化,懒加载,时间换空间,是由于getInstance方法是static,内部变量必须是static,uniqueInstance才是static,是被动的。但由于在getInstance时,可能A、B两个线程几乎同时进入,在A实例化未完成的情况下,B判断实例仍然为null,因此继续实例化,线程安全问题违背了单例的初衷。此时的解决办法是加锁synchronized,但加上之后会使效率急剧降低,因此有另一个方式:双重检查加锁。
饿汉式:饿急眼了来了就招呼,上来就把对象给实例化了,以后不用再实例化了,getInstance方法也必须是static,但期内部变量uniqueInstance的static属性是主动的,static变量在类装载时就初始化,且仅初始化一次,多个实例都会共享同一片内存空间,所以在一开始new的时候,static变量的特性保证的单例。因此不存在线程安全问题。空间换时间。
懒汉式
以时间换空间,在需要用到实例时再实例化,如果一直用不到就永远不实例化,节约空间;但牺牲了需要判断是否实例化的时间。
懒汉式本身存在线程安全问题,当两个线程同时判断uniqueInstance == null,则会同时进行实例化。可以通过synchronized方法的问题来解决,但这样太重了。
/**
* 懒汉式
*/
public class Singleton1 {
// 定义一个存储创建的类实例
private static Singleton1 uniqueInstance = null;
/**
* 私有构造方法
*/
private Singleton1() {
}
// 外部获取实例方法
public static synchronized Singleton1 getInstance() {
if (uniqueInstance == null) {
uniqueInstance = new Singleton1();
}
return uniqueInstance;
}
}
在内层加上synchronized同步代码块,但存在一个新问题,A线程拿到锁开始初始化,B线程已经判断了uniqueInstance == null,只需要等待A执行完,B继续去初始化,仍然线程不安全,因此在synchronized内部再加一层uniqueInstance == null的判断即可。
不在外层直接加synchronized的原因同不在方法上加一样,都太重了。每次进来不管三七二十一先竞争锁,没必要,有实例了直接返回就好了,要锁干嘛。
synchronized不能锁this,因为getInstance是静态方法,不能使用this。
但此时还有问题,最里面的new Singleton1()并非原子操作,其有三步:
- 分配内存空间
- 执行构造方法 初始化对象
- 把对象指向这个空间
正常执行顺序应该是1->2->3,但可能指令会被重排序为1->3->2;
synchronized只能保证有序性,但无法禁止指令重排,Java中只有volatile关键字可以禁止指令重排。
假设此时刚好A线程执行完3还没执行2,空间已经占了,但是还没初始化,当B线程进来判断17行:uniqueInstance == null,空间已经有了就不是null了,于是直接走到29行返回了对象,但此时对象还未初始化。因此 为了防止指令重排,必须给第4行uniqueInstance对象加上volatile关键字。
双重检测锁模式,也叫DCL懒汉式(DCL:Double Check Lock)
public class Singleton1 {
// 定义一个存储创建的类实例
private volatile static Singleton1 uniqueInstance = null;
/**
* 私有构造方法
*/
private Singleton1() {
}
// 外部获取实例方法
public static Singleton1 getInstance() {
// 外层判断
if (uniqueInstance == null) {
synchronized (Singleton1.class) {
// 内层判断
if (uniqueInstance == null) {
uniqueInstance = new Singleton1();// 非原子性操作
/**
* 1、分配内存空间
* 2、执行构造方法 初始化对象
* 3、把对象指向这个空间
*/
}
}
}
return uniqueInstance;
}
}
饿汉式
以空间换时间,类加载时就创建实例,如果一直不用就浪费空间;但需要使用时无需判断,已经存在,节省判断时间。
/**
* 饿汉式
*/
public class Singleton2 {
// 定义一个存储创建的类实例,并创建实例
private static Singleton2 uniqueInstance = new Singleton2();
/**
* 私有构造方法
*/
private Singleton2() {
}
// 外部获取实例方法
public static synchronized Singleton2 getInstance() {
return uniqueInstance;
}
}
静态内部类式
懒汉式的懒加载方式节约空间,但存在线程安全问题,即使存在解决办法也不是非常完美;
饿汉式虽然没有问题,但浪费空间;
静态内部类方式,既可以实现懒加载,又不浪费空间,代码如下:
/**
* 静态内部类式
*/
public class Singleton4 {
/**
* 私有静态内部类
*/
private static class SingletonHolder {
// 定义一个存储创建的类实例,并创建实例
private static Singleton4 uniqueInstance = new Singleton4();
}
/**
* 私有构造方法
*/
private Singleton4() {
}
// 外部获取实例方法
public static Singleton4 getInstance() {
return SingletonHolder.uniqueInstance;
}
}
本例采用了一个私有静态内部类的方式,private static class;该方式的特点是:静态内部类相当于外部类的成员,其属于外部类本身,而不属于外部类的对象,因此外部类实例化时静态内部类仍不会被加载。只有在静态内部类第一次真正被使用时才会被加载。
因此,内部类SingletonHolder中定义静态类实例,但在SingletonHolder未被调用时不会调用实例化;而当调用到getInstance方法时,才调用到SingletonHolder,于是uniqueInstance开始初始化;即实现了线程安全,又实现了懒加载。
枚举式
【此处非常奇怪,idea一直报错,最后重启了idea就好了。。。】
/**
* 枚举
*/
public enum Singleton3 {
UNIQUE_INSTANCE;
}
现在普遍来说,枚举是实现单例的最好方式,其由JVM从根本上保证了单例。前两种若使用反射或者序列化方式,仍可生成多个实例化对象。
例如:反射通过动态获取其类,然后调用newInstance方法实例化对象;序列化也同样,在其过程中会自动调用反射的newInstance方法实例化对象。若对枚举类使用反射并创建构造方法,jdk会抛出“Cannot reflectively create enum objects”的异常信息,无需我们做额外的特殊处理。
下面开始测试:
/**
* 测试类
*/
public class Test {
public static void main(String[] args) {
// 懒汉式
Singleton1 singleton1A = Singleton1.getInstance();
Singleton1 singleton1B = Singleton1.getInstance();
System.out.println("======= 懒汉式 =======");
System.out.println("singleton1A:"+singleton1A.hashCode());
System.out.println("singleton1B:"+singleton1B.hashCode());
// 饿汉式
Singleton2 singleton2A = Singleton2.getInstance();
Singleton2 singleton2B = Singleton2.getInstance();
System.out.println("======= 饿汉式 =======");
System.out.println("singleton2A:"+singleton2A.hashCode());
System.out.println("singleton2B:"+singleton2B.hashCode());
// 枚举式
Singleton3 singleton3A = Singleton3.UNIQUE_INSTANCE;
Singleton3 singleton3B = Singleton3.UNIQUE_INSTANCE;
System.out.println("======= 枚举式 =======");
System.out.println("singleton3A:"+singleton3A.hashCode());
System.out.println("singleton3B:"+singleton3B.hashCode());
// 静态内部类式
Singleton4 singleton4A = Singleton4.getInstance();
Singleton4 singleton4B = Singleton4.getInstance();
System.out.println("======= 静态内部类式 =======");
System.out.println("singleton4A:"+singleton4A.hashCode());
System.out.println("singleton4B:"+singleton4B.hashCode());
}
}
输出结果:
可以看到每种方式不同的实例间使用相同的hashCode