慢慢来,才比较快
单例模式属于对象创建型模式,它要求系统中只生成一个实例,提供了一种创建对象的最佳方式
?那么如何保证类中只有一个实例并且这个实例易于被访问呢
一个很好的解决的方法就是让这个类本身保存它的唯一实例,这个类需要保证没有其他实例被创建,并且它可以提供访问该实例的方法。
所以我们可以得出单例模式的定义:单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类称为单例类,它提供一个public方法。
因此单例模式的一个特点就是构造方法是私有的,这样就可以防止外界随意调用来产生实例(但是反射可以改变权限)
单例模式分为懒汉式和饿汉式,饿汉式是类加载的时候就初始化,而懒汉式是使用的时候才加载
单例模式的实现
饿汉式:
- 采用静态成员变量,在类加载的时候创建实例,如果用不到这个类会造成内存资源的浪费,因为单例实例引用不可变,所以是线程安全的
public class Singleton {
//1.私有构造方法
private Singleton() {}
//2.在本类中创建该类对象
private static Singleton instance = new Singleton();
public static Singleton getInstance(){
return instance;
}
}
- 第一种方法的变种
public class Singleton {
//私有构造方法
private Singleton() {}
//声明Singleton类型的变量
private static Singleton instance;
static {
instance = new Singleton();
}
public static Singleton getInstance(){
return instance;
}
}
- 通过枚举类实现单例模式,我们知道枚举反编译之后实际是由jvm创建一个final类去继承Enum类,通过static来加载,因此也属于饿汉式,枚举是线程安全的并且只会被装载一次,是唯一一种不会被破坏的单例模式。Java反编译以及语法糖。
public enum EnumInstance {
INSTANCE;
}
懒汉式:
public class Singleton {
private Singleton() {}
private static Singleton instance;
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
- 但是这种写法,很明显不是线程安全的。如果多个线程在该类初始化之前,有大于一个线程调用了getinstance方法且lazySingleton == null 判断条件都是正确的时候,这个时候就会导致new出多个LazySingleton实例。
- 解决的话可以在getInstance方法上加sychronized锁,但这种方式锁粒度太大,学过并发编程的同学应该都会记得有一个DCL(DoubleCheckedLocking),所以这里改进可以使用双重检查的方式
public class Singleton {
private Singleton(){}
private static Singleton instance;
public static Singleton getInstance(){
//先判断是否已经创建了实例
if(instance == null){
//当有多个线程进入这里时,加锁控制,只有一个线程可以进入下一步,如果条件成立则new出来
//一个实例,轮到其他的线程判断的时候自然就就为假了,问题大致解决。
synchronized (Singleton.class){
if (instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
对JMM熟悉的同学应该会发现,就算用了双重检查,也还是会出现问题。因为在编译过程中,为了提高性能,编译器和处理器常常会对指令做重排序,重排序对单线程并没有影响,但是在多线程情况下就有可能得出意料之外的结果。
eg:
instance = new Singleton();
执行的时候正常是这样的:
- 分配内存给这个对象
- 初始化对象
- 设置Singleton指向刚分配的内存地址。
但重排序之后就有可能变为这样:
- 分配内存给这个对象
- 设置Singleton指向刚分配的内存地址
- 初始化对象
这样就还是会造成多线程的问题,比如说有两个线程 t1 和 t2 ,t1进入了第一个if之中,并且拿到了锁,进行了new Singleton();语句,在加载构造类的实例的时候,设置Singleton指向刚分配的内存地址,但是还没有初始化对象。线程2判断 if(instance == null) 为假,直接返回了 instance ,但此时还没有初始化,这就出了问题。
当然这个很好改进,从禁用重排序方面下手,添加一个volatile。
private volatile static Singleton instance = null;
还有一个方法可以很好的避免这个问题:
定义一个静态内部类(基于类初始化的延迟加载),其静态字段实例化了一个单例。获取单例需要调用getInstance方法间接获取。
public class Singleton {
private Singleton() {}
private static class SingletonHolder{
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance(){
return SingletonHolder.INSTANCE;
}
}
单例模式的破坏和防范
序列化破坏:
对饿汉式和懒汉式都起效
public class Singleton implements Serializable {
//1.私有构造方法
private Singleton() {}
//2.在本类中创建该类对象
private static Singleton instance = new Singleton();
public static Singleton getInstance(){
return instance;
}
}
- 单例类先实现序列化接口
/**
* @Author: ekin
* @Date: 2021/7/3 17:45
*/
public class Client {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Singleton instance = Singleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton"));
oos.writeObject(instance);
File file = new File("singleton");
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
Singleton newInstance = (Singleton) ois.readObject();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}
- 测试
结果发现对象不一样,原因就涉及到序列化的底层原因了,我们先看解决方式:
单例类代码中添加下面这段代码
private Object readResolve() {
return instance;
}
结果:
原因:
- 在反序列化时,程序会检查如果单例类有readResolve方法时,则调用该方法的返回值,如果没有,则通过反射创建一个新实例
反射破坏:
public class Client {
public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class clazz = Singleton.class;
Constructor constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);//打开构造器权限
Singleton instance = Singleton.getInstance();
Singleton newInstance = (Singleton) constructor.newInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}
这里强行破开了private的构造方法的权限,使得能new出来一个单例实例。
解决:
饿汉式:
private Singleton() {
if (instance != null){
throw new RuntimeException("禁止反射调用");
}
}
懒汉式:
//当通过构造器产生实例时,该类没有实例
Singleton instance = constructor.newInstance();
Singleton newInstance = Singleton.getInstance();
- 暂时无法防止
枚举单例模式的好处
防范序列化破坏单例:
- 当进行反序列化读取时,调用顺序为
- 最后一个方法读取的仍然是原来的实例
防范反射破坏单例:
- 当进行反射的实例获取时,会出现异常
/**
* @Author: ekin
* @Date: 2021/7/3 20:52
*/
public class Client {
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
Class clazz = com.ekin.softwaredesign.singleton.demo6.Singleton.class;
Constructor constructor = clazz.getDeclaredConstructor();
constructor.setAccessible(true);
com.ekin.softwaredesign.singleton.demo1.Singleton instance = com.ekin.softwaredesign.singleton.demo1.Singleton.getInstance();
com.ekin.softwaredesign.singleton.demo1.Singleton newInstance = (Singleton) constructor.newInstance();
System.out.println(instance);
System.out.println(newInstance);
System.out.println(instance == newInstance);
}
}
- 原因是在 Constructor 中有这么一段话
if ((clazz.getModifiers() & Modifier.ENUM) != 0)
throw new IllegalArgumentException("Cannot reflectively create enum objects");
- 表示如果是枚举则不能反射。
单例模式总结:
优点:
- 在内存中只有一个实例,减少了资源的开销
- 可以避免对资源的多重占用
缺点:
- 没有接口,扩展困难
单例模式可以跟工厂模式搭配使用。