单例模式是指在内存中只会创建且仅创建一次对象的设计模式。在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象。
首先看一下原型模式:
@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
}
}
枚举的优势:
单例模式常见实现方式:饿汉式和懒汉式
懒汉式:在需要用到对象时才实例化对象,正确的实现方式是:Double Check + Lock,解决了并发安全和性能低下问题
饿汉式:在类加载时已经创建好该单例对象,在获取单例对象时直接返回对象即可,不会存在并发安全和性能问题。
懒汉式与饿汉式的选择:
最佳实现方式:枚举, 其代码精简,没有线程安全问题,且 Enum 类内部防止反射和反序列化时破坏单例