单例模式总结-java版

本文详细介绍了Java中单例模式的各种实现方式,包括懒汉式、饿汉式、同步懒汉式、双重检查锁定懒汉式、Initialization Demand Holder (IoDH)以及枚举类实现。对比了各种实现的优缺点,探讨了单例模式在多线程、序列化、反射等场景下的问题及解决方案。还讨论了单例模式的适用场景和潜在的内存管理问题。
摘要由CSDN通过智能技术生成

目录

单例模式简介

单例的具体模式介绍

测试类

懒汉式

饿汉式

同步懒汉式

双重检查锁定懒汉式

饿汉式单例类与懒汉式单例类比较

Initialization Demand Holder (IoDH)

枚举类

为什么要有枚单例

为什么枚举会满足线程安全、序列化等标准。

单例模式的优点

单例模式的缺点

单例模式的适用场景

单例模式与序列化

单例模式与反射

单例对象会被jvm的gc时回收吗


单例模式简介

在实际开发中,我们也经常遇到类似的情况,为了节约系统资源,有时需要确保系统中某个类只有唯一一个实例,当这个唯一实例创建成功之后,我们无法再创建一个同类型的其他对象,所有的操作都只能基于这个唯一实例。为了确保对象的唯一性,我们可以通过单例模式来实现,这就是单例模式的动机所在。

单例模式定义如下: 单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式是一种对象创建型模式。

单例模式有三个要点:一是某个类只能有一个实例;二是它必须自行创建这个实例;三是它必须自行向整个系统提供这个实例。

单例模式是结构最简单的设计模式一,在它的核心结构中只包含一个被称为单例类的特殊类。

单例模式结构图中只包含一个单例角色:

● Singleton(单例):在单例类的内部实现只生成一个实例(实例是static类型的),同时它提供一个静态的getInstance()工厂方法,让客户可以访问它的唯一实例;为了防止在外部对其实例化,将其构造函数设计为私有在单例类内部定义了一个Singleton类型的静态对象,作为外部共享的唯一实例。

单例的具体模式介绍

测试类

连续getInstance两次,看看构造函数被运用了几次

package algorithm.designpattern.p07singletonpattern;

public class Client {

	public static void main(String[] args) {
		EagerSingleton.getInstance();
		EagerSingleton.getInstance();
	}

}

懒汉式

为了实现实例的唯一性,我们通过如下三步来对该类进行设计:

(1) 由于每次使用new关键字来实例化类时都将产生一个新对象,为了确保实例的唯一性,我们需要禁止类的外部直接使用new来创建对象,因此需要将构造函数的可见性改为private,如下代码所示:

private LazySingleton() {……}

(2) 将构造函数改为private修饰后该如何创建对象呢?不要着急,虽然类的外部无法再使用new来创建对象,但是在类的内部还是可以创建的,可见性只对类外有效。因此,我们可以在LazySingleton中创建并保存这个唯一实例。为了让外界可以访问这个唯一实例,需要在LazySingleton中定义一个静态的LazySingleton类型的私有成员变量(此时只有static,没有final),如下代码所示:

private static LazySingleton singleton = null;

(3) 为了保证成员变量的封装性,我们将LazySingleton类型的singleton对象的可见性设置为private,但外界该如何使用该成员变量并何时实例化该成员变量呢?答案是增加一个公有的静态方法,如下代码所示:

public static LazySingleton getInstance()  
{  
    if (singleton== null)  
    {  
        singleton= new LazySingleton ();  
    }  
    return singleton;  
}

在getInstance()方法中首先判断singleton对象是否存在,如果不存在(即singleton== null),则使用new关键字创建一个新的LazySingleton类型的singleton对象,再返回新创建的singleton对象;否则直接返回已有的singleton对象。

需要注意的是getInstance()方法的修饰符,首先它应该是一个public方法,以便供外界其他对象使用,其次它使用了static关键字,即它是一个静态方法,在类外可以直接通过类名来访问,而无须创建LazySingleton对象,事实上在类外也无法创建LazySingleton对象,因为构造函数是私有的。

完整代码:

package algorithm.designpattern.p07singletonpattern;

public class LazySingleton {

	private static LazySingleton singleton=null;
	
	private LazySingleton(){
		System.out.println("单例对象创建");
	}
	
	public static LazySingleton getInstance(){
		System.out.println("获取单例对象");
		if(singleton==null){
			singleton=new LazySingleton();
		}
		return singleton;
	}
}
获取单例对象
单例对象创建
获取单例对象

普通的懒汉式的缺点:

在多线程中,如果两个线程同时执行getInstance方法,都singleton=null时,都进入到if语句内部,就有可能先后new了两个对象,使单例模式失败。在单线程中懒汉式是正确的,但是在多线程中是错误的,下面介绍4种多线程中正确的单例模式。

饿汉式

饿汉式单例类是实现起来最简单的单例类,由于在定义静态变量的时候实例化单例类,因此在类加载的时候就已经创建了单例对象。当类被加载时,静态变量instance会被初始化,此时类的私有构造函数会被调用,单例类的唯一实例将被创建。如果使用饿汉式单例来实现设计,则不会出现创建多个单例对象的情况,可确保单例对象的唯一性。

这里实例化的单例可以是final的,因为加载时就创建,之后不变

注意:此方法线程安全,实现简单,缺点是类加载时就被创建,浪费资源。

package algorithm.designpattern.p07singletonpattern;

public class EagerSingleton {

	private final static EagerSingleton singleton=new EagerSingleton();
	
	private EagerSingleton(){
		System.out.println("单例对象创建");
	}
	
	public static EagerSingleton getInstance(){
		System.out.println("获取单例对象");
		return singleton;
	}
}
单例对象创建
获取单例对象
获取单例对象

同步懒汉式

为了避免多个线程同时调用getInstance()方法,我们可以使用关键字synchronized

注意:该懒汉式单例类在getInstance()方法前面增加了关键字synchronized进行线程锁,以处理多个线程同时访问的问题。但是,上述代码虽然解决了线程安全问题,但是每次调用getInstance()时都需要进行线程锁定判断,在多线程高并发访问环境中,将会导致系统性能大大降低

package algorithm.designpattern.p07singletonpattern;

public class LazySingletonSynchronized {

	private static LazySingletonSynchronized singleton=null;
	
	private LazySingletonSynchronized(){
		System.out.println("单例对象创建");
	}
	
	public synchronized static LazySingletonSynchronized getInstance(){
		System.out.println("获取单例对象");
		if(singleton==null){
			singleton=new LazySingletonSynchronized();
		}
		return singleton;
	}
}
获取单例对象
单例对象创建
获取单例对象

双重检查锁定懒汉式

事实上,我们无须对整个getInstance()方法进行锁定,只需对其中的代码“instance = new LazySingleton();”进行锁定即可。因此getInstance()方法可以进行如下改进:

public static LazySingleton getInstance() {   
    if (instance == null) {  
        synchronized (LazySingleton.class) {  
            instance = new LazySingleton();   
        }  
    }  
    return instance;   
}

问题貌似得以解决,事实并非如此。如果使用以上代码来实现单例,还是会存在单例对象不唯一。原因如下:

假如在某一瞬间线程A和线程B都在调用getInstance()方法,此时instance对象为null值,均能通过instance == null的判断。由于实现了synchronized加锁机制,线程A进入synchronized锁定的代码中执行实例创建代码,线程B处于排队等待状态,必须等待线程A执行完毕后才可以进入synchronized锁定代码。但当A执行完毕时,线程B并不知道实例已经创建,将继续创建新的实例,导致产生多个单例对象,违背单例模式的设计思想,因此需要进行进一步改进,在synchronized中再进行一次(instance == null)判断,这种方式称为双重检查锁定(Double-Check Locking)

注意:需要注意的是,如果使用双重检查锁定来实现懒汉式单例类,需要在静态成员变量instance之前增加修饰符volatile,被volatile修饰的成员变量可以确保多个线程都能够正确处理,且该代码只能在JDK 1.5及以上版本中才能正确执行。

因为如果不加volatile关键字的话,instance=new Singleton中,包括开辟内存空间(1),初始化变量(2),将内存空间地址赋予instance(3)中的2和3会重排序,导致还没有初始化变量,instance就不为null,别的线程看到instance不为null,会把没有初始化的instance返回。

由于volatile关键字会屏蔽Java虚拟机所做的一些代码优化,可能会导致系统运行效率降低,因此即使使用双重检查锁定来实现单例模式也不是一种完美的实现方式。

package algorithm.designpattern.p07singletonpattern;

public class LazySingletonDoubleCheck {

	private volatile static LazySingletonDoubleCheck singleton=null;
	
	private LazySingletonDoubleCheck(){
		System.out.println("单例对象创建");
	}
	
	public static LazySingletonDoubleCheck getInstance(){
		System.out.println("获取单例对象");
		if(singleton==null){
			synchronized (LazySingletonDoubleCheck.class) {
				if(singleton==null){
					singleton=new LazySingletonDoubleCheck();
				}
			}			
		}
		return singleton;
	}
}
获取单例对象
单例对象创建
获取单例对象

饿汉式单例类与懒汉式单例类比较

饿汉式单例类在类被加载时就将自己实例化,它的优点在于无须考虑多线程访问问题,可以确保实例的唯一性;从调用速度和反应时间角度来讲,由于单例对象一开始就得以创建,因此要优于懒汉式单例。但是无论系统在运行时是否需要使用该单例对象,由于在类加载时该对象就需要创建,因此从资源利用效率角度来讲,饿汉式单例不及懒汉式单例,而且在系统加载时由于需要创建饿汉式单例对象,加载时间可能会比较长。

懒汉式单例类在第一次使用时创建,无须一直占用系统资源,实现了延迟加载,但是必须处理好多个线程同时访问的问题,特别是当单例类作为资源控制器,在实例化时必然涉及资源初始化,而资源初始化很有可能耗费大量时间,这意味着出现多线程同时首次引用此类的机率变得较大,需要通过双重检查锁定等机制进行控制,这将导致系统性能受到一定影响。

Initialization Demand Holder (IoDH)

饿汉式单例类不能实现延迟加载,不管将来用不用始终占据内存;懒汉式单例类线程安全控制烦琐,而且性能受影响。

有一种更好的方法,Initialization Demand Holder (IoDH)的技术。

注意:这种方法的java具体原理可见   https://blog.csdn.net/xushiyu1996818/article/details/103322155

在IoDH中,我们在单例类中增加一个静态(static)内部类,在该内部类中创建单例对象,再将该单例对象通过getInstance()方法返回给外部使用

注意:由于静态单例对象没有作为Singleton的成员变量直接实例化,因此类加载时不会实例化Singleton第一次调用getInstance()时将加载内部类HolderClass,在该内部类中定义了一个static类型的变量instance,此时会首先初始化这个成员变量,由Java虚拟机来保证其线程安全性,确保该成员变量只能初始化一次。由于getInstance()方法没有任何线程锁定,因此其性能不会造成任何影响。

通过使用IoDH,我们既可以实现延迟加载,又可以保证线程安全,不影响系统性能,不失为一种最好的Java语言单例模式实现方式(其缺点是与编程语言本身的特性相关,很多面向对象语言不支持IoDH)

注意:内部类 private static

内部类的变量 private static final

package algorithm.designpattern.p07singletonpattern;

public class LazySingletonHolder {

	
	private LazySingletonHolder(){
		System.out.println("单例对象创建");
	}
	
	private static class Holder{
		private static final LazySingletonHolder singleton=new LazySingletonHolder();
	}
	
	public static LazySingletonHolder getInstance(){
		System.out.println("获取单例对象");		
		return Holder.singleton;
	}
}

枚举类

public enum Singleton {

    INSTANCE;

    public void doSomething() {
        System.out.println("doSomething");
    }

}
调用方法:

public class Main {

    public static void main(String[] args) {
        Singleton.INSTANCE.doSomething();
    }

}

直接通过Singleton.INSTANCE.doSomething()的方式调用即可。方便、简洁又安全。

或者

public enum  EnumSingleton {
    INSTANCE;
    public EnumSingleton getInstance(){
        return INSTANCE;
    }
}

下面我们用一个枚举实现单个数据源例子来简单验证一下:
声明一个枚举,用于获取数据库连接。

public enum DataSourceEnum {
    DATASOURCE;
    private DBConnection connection = null;
    private DataSourceEnum() {
        connection = new DBConnection();
    }
    public DBConnection getConnection() {
        return connection;
    }
}  

模拟一个数据库连接类:

public class DBConnection {}

测试通过枚举获取的实例是否相同:

 public class Main {
    public static void main(String[] args) {
        DBConnection con1 = DataSourceEnum.DATASOURCE.getConnection();
        DBConnection con2 = DataSourceEnum.DATASOURCE.getConnection();
        System.out.println(con1 == con2);
    }
}

输出结果为:true  结果表明两次获取返回了相同的实例。

为什么要有枚单例

私有化构造器并不保险

《effective java》中只简单的提了几句话:“享有特权的客户端可以借助AccessibleObject.setAccessible方法,通过反射机制调用私有构造器。如果需要抵御这种攻击,可以修改构造器,让它在被要求创建第二个实例的时候抛出异常。”下面我以代码来演示一下,大家就能明白:

public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        Singleton s=Singleton.getInstance();
        Singleton sUsual=Singleton.getInstance();
        Constructor<Singleton> constructor=Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton sReflection=constructor.newInstance();
        System.out.println(s+"\n"+sUsual+"\n"+sReflection);
        System.out.println("正常情况下,实例化两个实例是否相同:"+(s==sUsual));
        System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:"+(s==sReflection));
    }

输出为:

com.lxp.pattern.singleton.Singleton@1540e19d
com.lxp.pattern.singleton.Singleton@1540e19d
com.lxp.pattern.singleton.Singleton@677327b6
正常情况下,实例化两个实例是否相同:true
通过反射攻击单例模式情况下,实例化两个实例是否相同:false

既然存在反射可以攻击的问题,就需要按照Joshua Bloch做说的,加个异常处理。这里我就不演示了,等会讲到枚举我再演示。

序列化问题

大家先看下面这个代码:

public class SerSingleton implements Serializable {
    private volatile static SerSingleton uniqueInstance;
    private  String content;
    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
    private SerSingleton() {
    }

    public static SerSingleton getInstance() {
        if (uniqueInstance == null) {
            synchronized (SerSingleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new SerSingleton();
                }
            }
        }
        return uniqueInstance;
    }


    public static void main(String[] args) throws IOException, ClassNotFoundException {
        SerSingleton s = SerSingleton.getInstance();
        s.setContent("单例序列化");
        System.out.println("序列化前读取其中的内容:"+s.getContent());
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerSingleton.obj"));
        oos.writeObject(s);
        oos.flush();
        oos.close();

        FileInputStream fis = new FileInputStream("SerSingleton.obj");
        ObjectInputStream ois = new ObjectInputStream(fis);
        SerSingleton s1 = (SerSingleton)ois.readObject();
        ois.close();
        System.out.println(s+"\n"+s1);
        System.out.println("序列化后读取其中的内容:"+s1.getContent());
        System.out.println("序列化前后两个是否同一个:"+(s==s1));
    }

}

先猜猜看输出结果:

序列化前读取其中的内容:单例序列化
com.lxp.pattern.singleton.SerSingleton@135fbaa4
com.lxp.pattern.singleton.SerSingleton@58372a00
序列化后读取其中的内容:单例序列化
序列化前后两个是否同一个:false

可以看出,序列化前后两个对象并不想等。为什么会出现这种问题呢?这个讲起来,又可以写一篇博客了,简单来说“任何一个readObject方法,不管是显式的还是默认的,它都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例。”当然,这个问题也是可以解决的,想详细了解的同学可以翻看《effective java》第77条:对于实例控制,枚举类型优于readResolve。

为什么枚举会满足线程安全、序列化等标准。

在JDK5 中提供了大量的语法糖,枚举就是其中一种。

所谓 语法糖(Syntactic Sugar),也称糖衣语法,是由英国计算机学家 Peter.J.Landin 发明的一个术语,指在计算机语言中添加的某种语法,这种语法对语言的功能并没有影响,但是但是更方便程序员使用。只是在编译器上做了手脚,却没有提供对应的指令集来处理它。

就拿枚举来说,其实Enum就是一个普通的类,它继承自java.lang.Enum类。

public enum DataSourceEnum {
    DATASOURCE;
}  

把上面枚举编译后的字节码反编译,得到的代码如下:

public final class DataSourceEnum extends Enum<DataSourceEnum> {
      public static final DataSourceEnum DATASOURCE;
      public static DataSourceEnum[] values();
      public static DataSourceEnum valueOf(String s);
      static {};
}

由反编译后的代码可知,DATASOURCE 被声明为 static 的,根据在【单例深思】饿汉式与类加载 中所描述的类加载过程,可以知道虚拟机会保证一个类的<clinit>() 方法在多线程环境中被正确的加锁、同步。所以,枚举实现是在实例化时是线程安全。

接下来看看序列化问题

Java规范中规定,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,因此在枚举类型的序列化和反序列化上,Java做了特殊的规定。

在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过 java.lang.Enum 的 valueOf() 方法来根据名字查找枚举对象。

也就是说,以下面枚举为例,序列化的时候只将 DATASOURCE 这个名称输出,反序列化的时候再通过这个名称,查找对应的枚举类型,因此反序列化后的实例也会和之前被序列化的对象实例相同。

public enum DataSourceEnum {
    DATASOURCE;
}  

由此可知,枚举天生保证序列化单例。

最后来看反射的问题

public enum  EnumSingleton {
    INSTANCE;
    public EnumSingleton getInstance(){
        return INSTANCE;
    }

    public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
        EnumSingleton singleton1=EnumSingleton.INSTANCE;
        EnumSingleton singleton2=EnumSingleton.INSTANCE;
        System.out.println("正常情况下,实例化两个实例是否相同:"+(singleton1==singleton2));
        Constructor<EnumSingleton> constructor= null;
        constructor = EnumSingleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        EnumSingleton singleton3= null;
        singleton3 = constructor.newInstance();
        System.out.println(singleton1+"\n"+singleton2+"\n"+singleton3);
        System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:"+(singleton1==singleton3));
    }
}
Exception in thread "main" java.lang.NoSuchMethodException: com.lxp.pattern.singleton.EnumSingleton.<init>()
    at java.lang.Class.getConstructor0(Class.java:3082)
    at java.lang.Class.getDeclaredConstructor(Class.java:2178)
    at com.lxp.pattern.singleton.EnumSingleton.main(EnumSingleton.java:20)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)
正常情况下,实例化两个实例是否相同:true

然后debug模式,可以发现是因为EnumSingleton.class.getDeclaredConstructors()获取所有构造器,会发现并没有我们所设置的无参构造器,只有一个参数为(String.class,int.class)构造器,然后看下Enum源码就明白,这两个参数是name和ordial两个属性:

public abstract class Enum<E extends Enum<E>>
            implements Comparable<E>, Serializable {
        private final String name;
        public final String name() {
            return name;
        }
        private final int ordinal;
        public final int ordinal() {
            return ordinal;
        }
        protected Enum(String name, int ordinal) {
            this.name = name;
            this.ordinal = ordinal;
        }
        //余下省略

   枚举Enum是个抽象类,其实一旦一个类声明为枚举,实际上就是继承了Enum,所以会有(String.class,int.class)的构造器。既然是可以获取到父类Enum的构造器,那你也许会说刚才我的反射是因为自身的类没有无参构造方法才导致的异常,并不能说单例枚举避免了反射攻击。好的,那我们就使用父类Enum的构造器,看看是什么情况:

public enum  EnumSingleton {
    INSTANCE;
    public EnumSingleton getInstance(){
        return INSTANCE;
    }

    public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
        EnumSingleton singleton1=EnumSingleton.INSTANCE;
        EnumSingleton singleton2=EnumSingleton.INSTANCE;
        System.out.println("正常情况下,实例化两个实例是否相同:"+(singleton1==singleton2));
        Constructor<EnumSingleton> constructor= null;
//        constructor = EnumSingleton.class.getDeclaredConstructor();
        constructor = EnumSingleton.class.getDeclaredConstructor(String.class,int.class);//其父类的构造器
        constructor.setAccessible(true);
        EnumSingleton singleton3= null;
        //singleton3 = constructor.newInstance();
        singleton3 = constructor.newInstance("testInstance",66);
        System.out.println(singleton1+"\n"+singleton2+"\n"+singleton3);
        System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:"+(singleton1==singleton3));
    }
}

然后咱们看运行结果:

正常情况下,实例化两个实例是否相同:true
Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
    at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
    at com.lxp.pattern.singleton.EnumSingleton.main(EnumSingleton.java:25)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.intellij.rt.execution.application.AppMain.main(AppMain.java:144)

继续报异常。之前是因为没有无参构造器,这次拿到了父类的构造器了,只是在执行第17行(我没有复制import等包,所以行号少于我自己运行的代码)时候抛出异常,说是不能够反射,我们看下Constructor类的newInstance方法源码:

@CallerSensitive
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }
        if ((clazz.getModifiers() & Modifier.ENUM) != 0)
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

请看黄颜色标注的第12行源码,说明反射在通过newInstance创建对象时,会检查该类是否ENUM修饰,如果是则抛出异常,反射失败。

单例模式的优点

(1) 单例模式提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它。

(2) 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象单例模式无疑可以提高系统的性能。

(3) 允许可变数目的实例。基于单例模式我们可以进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例,既节省系统资源,又解决了单例单例对象共享过多有损性能的问题。

单例模式的缺点

(1) 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。

(2) 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。

(3) 现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的共享对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。

单例模式的适用场景

(1) 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器或资源管理器,或者需要考虑资源消耗太大而只允许创建一个对象。

(2) 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。

1.项目中定义的配置文件

2.Servlet对象默认就是单例

3.线程池、数据库连接池

4.Spring中Bean对象默认就是单例

5.实现网站计数器

6.Jvm内置缓存框架(定义单例HashMap)

7.定义枚举常量信息

单例模式与序列化

/**
 * 序列化破解单列 (当类添加了 implements Serializable 的,都将可以破解单列)
 * @author wangsong
 * @date 2020/9/5 0005 10:26
 * @return
 * @version 1.0.0
 */
public class Test002 {
    public static void main(String[] args) throws Exception {
        // 1.需要将该对象序列化到本地存放
        FileOutputStream fos =  new FileOutputStream("d:/code/user.txt");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        Singleton01 instance1 = Singleton01.getInstance();
        oos.writeObject(instance1);
        oos.close();
        fos.close();
        //2.从硬盘中反序列化对象到内存中
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:/code/user.txt"));
        Singleton01 instance2 = (Singleton01) ois.readObject();
        /**
         * 将输出false, 单列对象 Singleton01 被重复创建, singleton01 在静态区的值被重新初始化,原数据将被破坏
         */
        System.out.println(instance1==instance2);

    }
}

如果单例的类实现了serializable接口,可以序列化,反序列化后,readObject会出现两个实例 

解决方法:加入readResolve()

在jdk中ObjectInputStream的类中有readUnshared()方法,就是如果被反序列化的对象的类存在readResolve这个方法,他会调用这个方法来返回一个“array”,然后浅拷贝一份,作为返回值,并且无视掉反序列化的值,即使那个字节码已经被解析。
所以,完整的单例模式是

public class SerSingleton implements Serializable {
    String name;
    private SerSingleton(){
        System.out.println("Singleton is creating");
    }

    private static SerSingleton instance = new SerSingleton();

    public static SerSingleton getInstance(){
        return instance;
    }

    public static void createString(){
        System.out.println("create string in singleton");
    }

    private Object readResolve(){
        System.out.println("read resolve");
        return instance;
    }
}

单例模式与反射

public class Test001 {
    public static void main(String[] args) throws Exception {

        // 使用反射机制创建我们的对象
        Class<?> aClass = Class.forName("com.xijia.Singleton01");
        //  getDeclaredConstructor();获取当前类(不包含父类),getConstructor 所有的  包含父类构造函数
        Constructor<?> constructor = aClass.getDeclaredConstructor();
        constructor.setAccessible(true);
        // 走无参构造函数 反射创建对象成功
        Singleton01 instance1 = (Singleton01) constructor.newInstance();
        Singleton01 instance2 = Singleton01.getInstance();
        /**
         * 将输出false, 单列对象 Singleton01 被重复创建, singleton01 在静态区的值被重新初始化,原数据将被破坏
         */
        System.out.println(instance1 == instance2);
    }
}

方法1

用反射获得私有构造函 数,然后用 constructor.newInstance 弄的两个对象也不是同一个对象,为了避免这个漏洞, 可以在私有的构造函数中加上 if(null!=instance) return new RuntimeException();

方法2

如果要抵御这种攻击,要防止构造函数被成功调用两次。需要在构造函数中对实例化次数进行统计,大于一次就抛出异常。

   private Singleton(){
        synchronized (Singleton.class) {
            if(count > 0){
                throw new RuntimeException("创建了两个实例");
            }
            count++;
        }
 
    }

单例对象会被jvm的gc时回收吗

hotspot虚拟机的垃圾收集算法使用根搜索算法。这个算法的基本思路是:对任何“活”的对象,一定能最终追溯到其存活在堆栈或静态存储区之中的引用。通过一系列名为根(GC Roots)的引用作为起点,从这些根开始搜索,经过一系列的路径,如果可以到达java堆中的对象,那么这个对象就是“活”的,是不可回收的。可以作为根的对象有:

虚拟机栈(栈桢中的本地变量表)中的引用的对象。

方法区中的类静态属性引用的对象。

方法区中的常量引用的对象。

本地方法栈中JNI的引用的对象。

 方法区是jvm的一块内存区域,用来存放类相关的信息。很明显,java中单例模式创建的对象被自己类中的静态属性所引用,符合第二条,因此,单例对象不会被jvm垃圾收集。

虽然jvm堆中的单例对象不会被垃圾收集,但是单例类本身如果长时间不用会不会被收集呢?因为jvm对方法区也是有垃圾收集机制的。如果单例类被收集,那么堆中的对象就会失去到根的路径,必然会被垃圾收集掉。对此,笔者查阅了hotspot虚拟机对方法区的垃圾收集方法,jvm卸载类的判定条件如下:

该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。

加载该类的ClassLoader已经被回收。

该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

只有三个条件都满足,jvm才会在垃圾收集的时候卸载类。显然,单例的类不满足条件一,因此单例类也不会被卸载。也就是说,只要单例类中的静态引用指向jvm堆中的单例对象,那么单例类和单例对象都不会被垃圾收集,依据根搜索算法,对象是否会被垃圾收集与未被使用时间长短无关,仅仅在于这个对象是不是“活”的。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值