单例模式
单例模式概述
单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。
- 单例类只能有一个实例
- 单例类必须自己创建自己的唯一实例
- 单例类必须给所有其他对象提供这一实例
单例模式类型
首先看一下原型模式:
@Data
public class Single {
private String singleName;
public static void main(String[] args) {
Single single1 = new Single();
Single single2 = new Single();
single1.setSingleName("");
System.out.println(single1.singleName);
System.out.println(single2.singleName);
}
}
测试结果:
single1
null
懒汉式
在真正需要使用对象时才去创建该单例类对象
/**
* 懒汉式单例
*/
@Data
public class LazySingle {
private String singleName;
private static LazySingle single=null;
public static LazySingle getInstance(){
if(single==null){
single=new LazySingle();
}
return single;
}
public static void main(String[] args) {
LazySingle single1=LazySingle.getInstance();
LazySingle single2=LazySingle.getInstance();
single1.setSingleName("懒汉式");
System.out.println(single1.singleName);
System.out.println(single2.singleName);
}
}
测试结果
懒汉式
懒汉式
解决懒汉式的线程安全问题
由代码可以看出,线程是不安全的,多线程情况下不能保证是单例的,解决方案肯定是加锁,但加锁会导致性能低下,所以解决方案应该兼顾性能和安全实现
解决方案为: Double Check(双重校验) + Lock(加锁)
public static LazySingle getInstance() {
if (singleton == null) { // 线程A和线程B同时看到singleton = null,如果不为null,则直接返回singleton
synchronized(LazySingle.class) { // 线程A或线程B获得该锁进行初始化
if (singleton == null) { // 其中一个线程进入该分支,另外一个线程则不会进入该分支
singleton = new LazySingle();
}
}
}
return singleton;
}
但在JVM运行过程中会有一个问题:
指令重排
JVM在保证最终结果正确的情况下,可以不按照程序编码的顺序执行语句,尽可能提高程序的性能
JVM创建一个对象会经过3步:
- 为对象分配内存空间
- 初始化对象
- 将对象指向分配好的内存空间
在这三步中,第2、3步有可能会发生指令重排现象,创建对象的顺序变为1-3-2,会导致多个线程获取对象时,有可能线程A创建对象的过程中,执行了1、3步骤,线程B判断singleton已经不为空,获取到未初始化的singleton对象,就会报NPE异常。
解决方案:使用volatile关键字修饰
可以保证其指令执行的顺序与代码顺序一致,不会发生顺序变换
可以保证其内存可见性,即每一时刻线程读取到该变量的值都是内存中最新的那个值,线程每次操作该变量都需要先读取该变量。
最终解决方案如下:
/**
* 懒汉式单例
*/
@Data
public class LazySingle {
private String singleName;
private static volatile LazySingle single=null;
public static LazySingle getInstance(){
if(single==null){
synchronized (LazySingle.class){
if(single == null){
single=new LazySingle();
}
}
}
return single;
}
}
饿汉式
饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以天生是线程安全的
/**
* 饿汉式单例
*/
@Data
public class HungrySingle {
private String singleName;
private static final HungrySingle singleton=new HungrySingle();
public static HungrySingle getInstance() {
return singleton;
}
public static void main(String[] args) {
HungrySingle single1 = HungrySingle.getInstance();
HungrySingle single2 = HungrySingle.getInstance();
single1.setSingleName("饿汉式");
System.out.println(single1.singleName);
System.out.println(single2.singleName);
}
}
测试结果
饿汉式
饿汉式
破坏单例的情况
java的反射和序列化可以破坏单例模式(饿汉式和懒汉式)
- 使用反射破坏单例模式(演示饿汉式)
try {
//获取类的显式构造器
Constructor<HungrySingle> constructor = HungrySingle.class.getDeclaredConstructor();
// 可访问私有构造器
constructor.setAccessible(true);
HungrySingle singleton1 = constructor.newInstance();
HungrySingle singleton2 = constructor.newInstance();
System.out.println(singleton1==singleton2); //false
} catch (Exception e) {
e.printStackTrace();
}
- 使用序列化与反序列化破坏单例模式(演示懒汉式)
try {
File file = new File("Singleton.txt");
//创建输出流
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(file));
//将单例对象写到文件中 序列化
oos.writeObject(LazySingle.getInstance());
//从文件读取单例对象
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
//反序列化得到对象lazySingle
LazySingle lazySingle=(LazySingle)ois.readObject();
System.out.println(lazySingle==LazySingle.getInstance()); //false
} catch (Exception e) {
e.printStackTrace();
}
枚举实现单例模式
public enum Sex {
MALE,FEMALE;
public static void main(String[] args) {
Sex male1 = Sex.MALE;
Sex male2 = Sex.MALE;
System.out.println(male1==male2);//true
}
}
枚举的优势:
- 代码简洁
- 不需要额外操作保证线程安全和对象单一性
- 反射、序列化不能破坏枚举类的单例模式
- 枚举类默认继承了 Enum 类,在利用反射调用 newInstance() 时,会判断该类是否是一个枚举类,如果是,则抛出异常。
- 在序列化和反序列化的过程中,只是写出和读入了枚举类型和名字,没有任何关于对象的操作。
总结
-
单例模式常见实现方式:饿汉式和懒汉式
-
懒汉式:在需要用到对象时才实例化对象,正确的实现方式是:Double Check + Lock,解决了并发安全和性能低下问题
-
饿汉式:在类加载时已经创建好该单例对象,在获取单例对象时直接返回对象即可,不会存在并发安全和性能问题。
-
懒汉式与饿汉式的选择:
- 对内存要求非常高,使用懒汉式写法,可以在特定时候才创建该对象;
- 对内存要求不高,使用饿汉式写法,因为简单不易出错,且没有任何线程安全和性能问题
-
最佳实现方式:枚举, 其代码精简,没有线程安全问题,且 Enum 类内部防止反射和反序列化时破坏单例