设计模式-单例模式及应用

单例模式

所谓的单例模式,就是采取一定的方法保证在整个系统中,对某个类只有一个对象实例;并且该类只提供一个获取该对象实例的方法。

单例模式三要素

  • 构造方法私有化;
  • 实例化变量引用私有化;
  • 暴露公共方法获取实例。

Singleton(单例):在单例类的内部实现只生成一个实例,同时它提供一个静态的 getInstance() 工厂方法,让客户可以访问它的唯一实例;为了防止在外部对其实例化,将其构造函数设计为私有;在单例类内部定义了一个 Singleton 类型的静态对象,作为外部共享的唯一实例。围绕所介绍的三要素,下面介绍下单例模式的8中写法,并列举其优缺点。

8种创建单例对象示例

1.饿汉式-静态常量 (线程安全)
class Singleton {
	
	private Singleton() {
		
	}

	private final static Singleton instance = new Singleton();
	
	public static Singleton getInstance() {
		return instance;
	}	
}

优点:简单,使用时没有延迟;在类装载时就完成实例化,天生的线程安全

缺点:在类装载的时候就完成实例化,没有达到 Lazy Loading的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费。

2.饿汉式-静态变量 (线程安全)
class Singleton {
	
	private Singleton() {
		
	}
	
	private  static Singleton instance;
	
	static { 
		instance = new Singleton();
	}
	
	public static Singleton getInstance() {
		return instance;
	}
	
}

优缺点说明:这种方式和上面的方式其实类似,只不过将类实例化的过程放在了静态代码块中,也是在类裝载的时候,就执
行静态代码块中的代码,初始化类的实例。优缺点和上面是一样

3.懒汉式(线程不安全)
class Singleton {
	private static Singleton instance;
	
	private Singleton() {}
	
	//提供一个静态的公有方法,当使用到该方法时,才去创建 instance
	//即懒汉式
	public static Singleton getInstance() {
		if(instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
}

优点:起到了懒加载的目的,避免了内存资源的浪费;
缺点:多线程条件下会造成线程不安全现象的发生;在多线程条件下,一个线程进入了if(instance == null)判断语句,还未往下执行,另外一个线程也通过了这个判断语句,这是便会产生多个实例;在实际开发中不要使用此种方式。

4.懒汉式-同步方法(线程安全)
class Singleton {
	private static Singleton instance;
	
	private Singleton() {}
	
	//提供一个静态的公有方法,加入同步处理的代码,解决线程安全问题
	public static synchronized Singleton getInstance() {
		if(instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
}

优点:解决了懒汉式线程安全的问题,通过静态同步方法的方式控制实例对象只创建一次;
缺点:效率太低,每个线程在获取实例时候,要同步的方式获取,不推荐使用此种方式。

5.懒汉式-同步代码块(线程安全)
class Singleton {
	private static Singleton instance;

	private Singleton() {}

	//提供一个静态的公有方法,加入同步处理的代码,解决线程安全问题
	public static Singleton getInstance() {
		if(instance == null) {
			synchronized (Singleto.class){
				instance = new Singleton();
			}
		}
		return instance;
	}
}

优缺点同上;

6.双重锁检查(线程安全)
class Singleton {
	private static volatile Singleton instance;
	
	private Singleton() {}
	
	//提供一个静态的公有方法,加入双重检查代码,解决线程安全问题, 同时解决懒加载问题
	//同时保证了效率, 推荐使用
	public static synchronized Singleton getInstance() {
		if(instance == null) {
			synchronized (Singleton.class) {
				if(instance == null) {
					instance = new Singleton();
				}
			}
			
		}
		return instance;
	}
}

优点:Double-Check 线程安全、延时加载、效率高,推荐使用此种方式。
由于 JVM 具有指令重排的特性,在多线程环境下可能出现 singleton 已经赋值但还没初始化的情况,导致一个线程获得还没有初始化的实例。volatile 关键字的作用:

  • 保证了不同线程对这个变量进行操作时的可见性
  • 禁止进行指令重排序
7.静态内部类(线程安全)
class Singleton {
	//构造器私有化
	private Singleton() {}
	
	//写一个静态内部类,该类中有一个静态属性 Singleton
	private static class SingletonInstance {
		private static final Singleton INSTANCE = new Singleton(); 
	}
	
	//提供一个静态的公有方法,直接返回SingletonInstance.INSTANCE
	public static synchronized Singleton getInstance() {
		
		return SingletonInstance.INSTANCE;
	}
}

此种方式是利用JVM类的装载机制,来保证初始化实例时,只有一个线程;
静态内部类方式在singleton类被装载时不会立即执行实例化,而是在需要实力化时,调用getInstance方法时,才会去装载SingletonInstance类,从而完成Singleton对象的实例化;
类的静态属性只会在第一次类加载的时候初始化,JVM会帮助我们保证线程的安全性,在类进行初始化时,别的线程无法进入;

优点:避免线程不安全,利用静态内部类的特点来实现延迟加载,效率高;推荐使用。

8.枚举(线程安全)
enum Singleton {
	INSTANCE; 
	public void sayHello() {
		System.out.println("hello world~~~");
	}
}

优缺点:通过JDK1.5中,添加的枚举来实现单例模式,写法简单且能避免多线程同步问题,而且能防止反序列化重新创建新的对象。推荐使用。

单例模式的安全性

单例模式的目标是,任何时候该类都只有唯一的一个对象。但是上面我们写的大部分单例模式都存在漏洞,例如序列化攻击,反射攻击等,被攻击时会产生多个对象,破坏了单例模式。

序列化攻击

回顾Java反序列化主要要点:

  • 需要实现java.io.Serializable接口,否则会抛出NotSerializableException异常

  • 需要显示声明一个serialVersionUID变量,如果没有设置,Java序列化机制会根据编译的class生成一个serialVersionUID作为序列化版本的比较(主要为了验证其一致性),如果检查到序列化和反序列化serialVersionUID不同,则抛出异常。

  • 当某个字段被声明为transient后,默认序列化机制就会忽略该字段,反序列化后自动获得0或者null值,静态成员不参与序列化

  • 每个类可以实现readObjectwriteObject方法实现自己的序列化策略,即使是transient修饰的成员变量也可以手动调用ObjectOutputStreamwriteInt等方法将这个成员变量序列化。

  • 任何一个readObject方法,不管是显式的还是默认的,它都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例

  • 每个类可以实现private Object readResolve()方法,在调用readObject方法之后,如果存在readResolve方法则自动调用该方法,readResolve将对readObject的结果进行处理,而最终readResolve的处理结果将作为readObject的结果返回。readResolve的目的是保护性恢复对象,其最重要的应用就是保护性恢复单例、枚举类型的对象.(重点)

  • Serializable接口是一个标记接口,可自动实现序列化,而Externalizable继承自Serializable,它强制必须手动实现序列化和反序列化算法,相对来说更加高效

public class HungrySingleton {
    private static final HungrySingleton instance = new HungrySingleton();
    private HungrySingleton() {
    }
    public static HungrySingleton getInstance() {
        return instance;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        HungrySingleton singleton = HungrySingleton.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
        // 对象转为二进制流 写入文件(序列化)
        oos.writeObject(singleton); 

        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton_file"));
        // 二进制流转为对象 (反序列化)
        HungrySingleton newSingleton = (HungrySingleton) ois.readObject(); 

        System.out.println(singleton);
        System.out.println(newSingleton);
        System.out.println(singleton == newSingleton);
    }
}

执行结果:

com.atguigu.singleton.serializableattack.HungrySingleton@279f2327
com.atguigu.singleton.serializableattack.HungrySingleton@4783da3f
false

通过结果发现,反序列化的对象和我们生成的单例对象不是同一个对象。这就没有达到我们单例的目的。

序列化问题解决

通过前面序列化要点介绍,方法readResolve,在调用readObject方法之后,如果存在readResolve方法则自动调用该方法,readResolve将对readObject的结果进行处理,而最终readResolve的处理结果将作为readObject的结果返回. 那这里我们就重写 readResolve方法,直接返回instance实例。

private static final HungrySingleton instance = new HungrySingleton();


	private HungrySingleton() {
	}

	public static HungrySingleton getInstance() {
		return instance;
	}

	private Object readResolve() {
		System.out.println("readObject after....");
		return instance;
	}

	public static void main(String[] args) throws IOException, ClassNotFoundException {
		HungrySingleton singleton = HungrySingleton.getInstance();
		ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
		//对象转为二进制流 写入文件(序列化)
		oos.writeObject(singleton);

		ObjectInputStream ois = new ObjectInputStream(new FileInputStream("singleton_file"));
		// 二进制流转为对象 (反序列化)
		HungrySingleton newSingleton = (HungrySingleton) ois.readObject();
		System.out.println("readResolve after....");

		System.out.println(singleton);
		System.out.println(newSingleton);
		System.out.println(singleton == newSingleton);
	}

执行结果:

readObject after....
readResolve after....
com.atguigu.singleton.serializableattack.HungrySingleton@279f2327
com.atguigu.singleton.serializableattack.HungrySingleton@279f2327
true

可以看到此时,返回的对象地址是相同的。

反射攻击

private static final HungrySingleton instance = new HungrySingleton();

	private HungrySingleton() {
	}

	public static HungrySingleton getInstance() {
		return instance;
	}

	public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
		HungrySingleton instance = HungrySingleton.getInstance();
		Constructor constructor = HungrySingleton.class.getDeclaredConstructor();
		constructor.setAccessible(true);    // 获得权限
		HungrySingleton newInstance = (HungrySingleton) constructor.newInstance();

		System.out.println(instance);
		System.out.println(newInstance);
		System.out.println(instance == newInstance);
	}

执行结果:

com.atguigu.singleton.reflectattack.HungrySingleton@6f94fa3e
com.atguigu.singleton.reflectattack.HungrySingleton@5e481248
false

使用反射,可以通过Class对象来创建行的指定类的对象实例,在单例模式中构造器都是私有的,而反射可以通过构造器对象调用 setAccessible(true) 来获得权限,这样就可以创建多个对象,来破坏单例模式了,避免通过反射创建单例对象的方法,可以在构造器中加一层判断。

private HungrySingleton() {
		// instance 不为空,说明单例对象已经存在
		if (instance != null) {
			throw new RuntimeException("单例模式禁止反射调用!");
		}
	}

执行结果:

Exception in thread "main" java.lang.reflect.InvocationTargetException
	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
	at com.atguigu.singleton.reflectattack.HungrySingleton.main(HungrySingleton.java:30)
Caused by: java.lang.RuntimeException: 单例模式禁止反射调用!
	at com.atguigu.singleton.reflectattack.HungrySingleton.<init>(HungrySingleton.java:18)
	... 5 more

反射最调的还是构造方法,这里通过饿汉式的方式,在类加载的时候单例对象已经创建完成,所有这里我们通过构造方法的方式可以直接判断是否存在,来控制外部不能通过反射创建对象。

注意: 而此种方式使用时,需要注意的事仅针对饿汉式,懒汉式的方式不适用,懒汉式是懒加载的方式,那在我们使用之前,不管通过反射创建多少个对象,我们都不清楚。

终极方案枚举

列举上述的种种单例实现方式,有很多漏洞需要我们注意。为什么我们这里推荐使用枚举的方式。

public enum EnumSingleton implements Serializable {
	INSTANCE;   // 单例对象
	private String content;

	public String getContent() {
		return content;
	}

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

	private EnumSingleton() {
	}

	public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
		EnumSingleton singleton1 = EnumSingleton.INSTANCE;
		singleton1.setContent("枚举单例序列化");
		System.out.println("枚举序列化前读取其中的内容:" + singleton1.getContent());
		ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerEnumSingleton.obj"));
		oos.writeObject(singleton1);
		oos.flush();
		oos.close();

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

		Constructor<EnumSingleton> constructor = EnumSingleton.class.getDeclaredConstructor();
		constructor.setAccessible(true);
		EnumSingleton singleton3 = constructor.newInstance(); // 通过反射创建对象
		System.out.println("反射后读取其中的内容:" + singleton3.getContent());
		System.out.println("反射前后两个是否同一个:" + (singleton1 == singleton3));
	}

执行结果:

枚举序列化前读取其中的内容:枚举单例序列化
INSTANCE
INSTANCE
枚举序列化后读取其中的内容:枚举单例序列化
枚举序列化前后两个是否同一个:true
Exception in thread "main" java.lang.NoSuchMethodException: com.atguigu.singleton.EnumSingleton.<init>()
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.getDeclaredConstructor(Class.java:2178)
	at com.atguigu.singleton.EnumSingleton.main(EnumSingleton.java:45)

可以看到,序列化后的对象地址没有变,而且当我们使用反射试图创建对象是,报错。
底层实现,我们可以通过 javap -c **.class命令,查看字节码文件,即构成 Java类 字节码的指令

## singleton: javap -c EnumSingleton.class
Compiled from "EnumSingleton.java"
public final class com.atguigu.singleton.EnumSingleton extends java.lang.Enum<com.atguigu.singleton.EnumSingleton> implements java.io.Serializable {
  public static final com.atguigu.singleton.EnumSingleton INSTANCE;

可以看到,我们通过生成的字节码文件看到,public final class T extends Enum,说明我们将类定义为enume类型时,编译器底层会帮助我们创建一个final类型的类继承Enume类,所以我们的枚举类不能被继承。
枚举能够避免反射的攻击,是因为反射不支持创建枚举类型对象。

综上:枚举的好处可以列举为一下几个点:

  • 写法简单,枚举的写法不同于其他几种方式,需要大量的代码;
  • 线程安全 单例对象 INSTANCE,通过字节码文件得到,是通过static修饰的,类加载之后初始化JVM可以保证线程安全;
  • 懒加载 JAVA类在引用到时,才会去进行类加载。所以枚举单例也具有懒加载的效果;
  • 枚举能避免序列化、反射攻击。
单例模式总结
  • 单例模式提供了对唯一实例的受控访问;
  • 单例模式保证了系统内存中该类只用一个对象,节省了系统资源,对一些需要频繁创建销毁的对象,使用单例可以提升系统性能;
单例模式应用

JDK Runtime (饿汉式)

public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    public static Runtime getRuntime() {
        return currentRuntime;
    }

    private Runtime() {
    }
    //....
}

JDK Runtime类代表着Java程序的运行时环境,每个Java程序都有一个Runtime实例,该类会被自动创建,我们可以通过 Runtime.getRuntime() 方法来获取当前程序的Runtime实例。一旦得到了一个当前的Runtime对象的引用,就可以调用Runtime对象的方法去控制Java虚拟机的状态和行为。

✨✨ 欢迎🔔 订阅个人的微信公众号
个人工作号
✨✨ 个人GitHub地址
GitHub

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值