单列模式(SIngleton pattern)是java中最基础的一种设计模式,他的作用是保证一个类仅有一个实例,并且提供一个访问它的全局访问点,避免重复创建对象,节省系统资源。以下通过五种方式来实现,可根据不同的场景选用。
一、饿汉式
public class Singleton {
//类加载时就创建本类的对象
private static Singleton instance = new Singleton();
//构造器私有化,使外部不能通过new来创建对象
private Singleton(){}
//外部可通过该方法来获取唯一实例
public static Singleton getInstance(){
return instance;
}
}
之所以称之为饿汉式,可以理解为:饿了,马上就要吃,在类加载的时候就创建对象,用时直接取就行。
缺点:在还不需要此类的实例的时候就已经创建好了,没有起到 lazy loading 的效果。
优点:简单、安全可靠
二、懒汉式
1、线程不安全
public class Singleton {
//用静态变量来记录本类实例
private static Singleton instance;
//获取实例
public static Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
//构造器私有化
private Singleton(){}
}
可以理解为:很懒、需要用时再创建,当一个对象使用频率不是很高、占用的内存特别大,饿汉式就明显不合适了,这时就需要懒汉式这种懒加载的思想,等到程序需要实例的时候再创建。上面这种实现方式是存在线不程安全问题,在并发获取实例的时候,可能会存在构建多个实例的情况,通过下面的改进可以实现线程安全:
2、线程安全
public class Singleton {
private static volatile Singleton instance;
//构造器私有化,禁止外部通过new创建实例
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){
synchronized(Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
与上面的相比,这里使用了双重效验的方式,对懒汉式线程安全化,通过加锁,可以保证同时只有一个线程走到第二个判空代码中去,这样保证了只创建 一个实例。这里还用到了volatile关键字来修饰singleton,其最关键的作用是防止指令重排。
三、静态内部类式
public class Singleton {
private static class SingletonHolder {
private static Singleton instance = new Singleton();
}
private Singleton(){}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
}
通过这种静态内部类的方式实现的单例模式是线程安全的,静态的内部类在Singleton加载的时候是不会加载的。在调用getInstance时才会创建Singleton的实例,起到了懒加载的作用。
以上方式实现单例模式看起来已经完美,但是还会存在一些安全问题,即反射攻击、反序列化攻击,下面就通过这两种方式来获取新的实例,并验证原来的实列和新的实例是否是一个实例。
反射获取新实例
public class Singleton {
private static class SingletonHolder {
private static Singleton instance = new Singleton();
}
private Singleton(){}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
Constructor<Singleton> constructor;
try {
constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton newSingleton = constructor.newInstance();
System.out.println(singleton == newSingleton);
}catch (Exception e){
}
}
}
通过结果可以看出这两个实例不是同一个实例,这和我们的单例模式不符。
除了反射获取还可以通过反序列化的方式获取到新的实例:
反序列化方式获取新实例
public class Singleton implements Serializable {
private static class SingletonHolder {
private static Singleton instance = new Singleton();
}
private Singleton(){}
public static Singleton getInstance(){
return SingletonHolder.instance;
}
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
try {
ObjectOutputStream oos = new ObjectOutputStream(
new FileOutputStream(new File("./Singleton.txt")));
//把对象写入磁盘
oos.writeObject(singleton);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("./Singleton.txt")));
//从磁盘中读取对象
Singleton newSingleton = (Singleton)ois.readObject();
//比较
System.out.println(singleton == newSingleton);
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
和反射获取的结果是一样的,下面通过枚举的方式实现解决上面这两个问题。
四、枚举方式
public enum EnumSingleton {
INSTANCE;
private EnumSingleton(){
System.out.println("init");
}
public void sayHello(){
System.out.println("hello");
}
public static void main(String[] args) {
for (int i =0;i<5;i++){
EnumSingleton.INSTANCE.sayHello();
}
}
}
结果可以看出实际只初始化了一次,这种方式也可以实现单例。利用枚举的特性,让JVM来帮我们保证线程安全和单一实例的问题。除此之外,写法还特别简单。
总结
以上通过了多中方式实现了单例模式,分析其的利弊,可根据场景选用。