设计模式之单例模式
单例模式是一种常用的软件设计模式,其定义是单例对象的类只能允许一个实例存在。许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。
优点:系统内存中该类只存在一个对象,节省了系统资源,对于一些需要频繁创建销毁的对象,使用单例模式可以提高系统性能。
缺点:可读性差,当想实例化一个单例类的时候,必须要记住使用相应的获取对象的方法,而不是使用 new
,可能会给其他开发人员造成困扰,特别是看不到源码的时候。单例模式中在使用反射时,对象也可能不唯一,所以我们要注意不要认定单例模式唯一是其好处,从而生成刻板印象。
适用场合
- 需要频繁的进行创建和销毁的对象
- 创建对象时耗时过多或耗费资源过多,但又经常用到的对象;
- 工具类对象或者频繁访问数据库或文件的对象。
基本实现思路
单例模式要求类能够有返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名称)。
单例的实现主要是通过以下步骤:
(1)将构造方法私有化,使其不能在类的外部通过new关键字实例化该类对象。
(2)在该类内部产生一个唯一的实例化对象,并且将其封装为private static类型。
(3)定义一个静态方法返回这个唯一对象。
注意事项
单例模式在多线程的应用场合下必须小心使用。如果当唯一实例尚未创建时,有两个线程同时调用创建方法,那么它们同时没有检测到唯一实例的存在,从而同时各自创建了一个实例,这样就有两个实例被构造出来,从而违反了单例模式中实例唯一的原则。 解决这个问题的办法是为指示类是否已经实例化的变量提供一个互斥锁(虽然这样会降低效率)。
饿汉式(静态常量)[可用]
public class Singleton{
private final static Singleton singleton = new Singleton();
private Singleton(){}
public static Singleton getInstance(){
return singleton;
}
}
优点:这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。
缺点:在类装载的时候就完成实例化,没有达到懒加载的效果。如果从始至终从未使用过这个实例,则会造成内存的浪费
饿汉式(静态代码块)[可用]
public class Singleton {
private static Singleton singleton;
static {
instance = new Singleton();
}
private Singleton() {}
public Singleton getInstance() {
return singleton;
}
}
和上面的方式类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。
优缺点和上面是一样的。
懒汉式(线程不安全)[不可用]
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
这种写法起到了懒加载的效果,但只能在单线程下使用。如果在多线程下,一个线程进入了 if (singleton == null)
判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。
懒汉式(线程安全,同步方法)[不推荐用]
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
解决上面第三种实现方式的线程不安全问题,做个线程同步就可以了,于是就对 getInstance()
方法进行了线程同步。
缺点:同步效率低,每个线程在想获得类的实例时候,执行 getInstance()
方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接 return
就行了。
懒汉式(线程安全,同步代码块)[不可用]
public class Singleton {
private static Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
singleton = new Singleton();
}
}
return singleton;
}
}
由于第四种实现方式同步效率低,所以摒弃同步方法,改为同步产生实例化的的代码块。但是这种同步并不能起到线程同步的作用。跟第3种实现方式遇到的情形一致,假如一个线程进入了 if (singleton == null)
判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。
懒汉式双检锁[推荐用]
public class Singleton {
private static volatile Singleton singleton;
private Singleton() {}
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
这种方式跟饿汉式方式采用的机制类似,但又有不同。两者都是采用了类装载的机制来保证初始化实例时只有一个线程。不同的地方在饿汉式方式是只要 Singleton
类被装载就会实例化,没有懒加载的作用,而静态内部类方式在 Singleton
类被装载时并不会立即实例化,而是在需要实例化时,调用 getInstance
方法,才会装载 SingletonInstance
类,从而完成 Singleton
的实例化。
类的静态属性只会在第一次加载类的时候初始化,所以在这里,JVM
帮助我们保证了线程的安全性,在类进行初始化时,别的线程是无法进入的。
优点:避免了线程不安全,延迟加载,效率高。
懒汉式三重检查[推荐用]
public class LazyMan {
//设置标志位,防止反射通过无参构造新建对象
private static boolean flag=false;
private LazyMan(){
synchronized (LazyMan.class){
if(flag==false){
flag=true;
} else {
throw new RuntimeException("不要试图用反射破解单例");
}
}
}
private volatile static LazyMan lazyMan;
public static LazyMan getInstance(){
if(lazyMan==null){
synchronized (LazyMan.class){
if(lazyMan==null){
lazyMan=new LazyMan();
}
}
}
return lazyMan;
}
public static void main(String[] args) throws IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException, NoSuchFieldException {
//如果获取了该标志位的属性,还是可以通过反射篡改该private属性
Field flag = LazyMan.class.getDeclaredField("flag");
flag.setAccessible(true); //篡改属性
Constructor<LazyMan> declaredConstructor= LazyMan.class.getDeclaredConstructor(); //获取无参构造
declaredConstructor.setAccessible(true);
LazyMan l1 = declaredConstructor.newInstance(); // 通过newInstance方法新建对象
flag.set(l1,false);
System.out.println(l1.hashCode()); //后台输出该对象的哈希值
LazyMan l2 = LazyMan.getInstance(); //通过传统的方式创建,不通过反射创建
System.out.println(l2.hashCode()); //后台输出正常创建对象的哈希值,输出结果为两者不一致
}
}
这种方式可以避免通过反获取该对象的无参构造函数,但是魔高一尺,道高一丈,只需要获取设置标志位的字段名,通过getDeclaredField方法就可以获得该字段,通过setAccessible方法就可以篡改该字段属性,一样来拿无参数构造就行,无视标志位,直接创建新对象,此时发生单例模式对象并不单一,所以我们不要有传统刻板的印象,有的时候多了解一些,对自己眼界很有帮助。
静态内部类单例
//静态内部类
public class Holder {
private Holder(){
}
private static Holder getInstance(){
return InnerClasses.holder;
}
public static class InnerClasses{
private static final Holder holder=new Holder();
}
}
静态内部类的方式效果类似双检锁,但实现更简单。但这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
静态内部类在外部类加载的时候不会被加载,只有外部类中用到内部类时候才会被加载。由于静态内部类没有使用任何锁机制,所以性能优于双重检查实现方式。
枚举单例
public enum SingletonEnum {
INSTANCE;
private Object data = new Object();
public Object getData(){
return data;
}
public static SingletonEnum getInstance(){
return INSTANCE;
}
}
jvm判断了枚举无法反射获取对象,无法序列化,防止了反射破坏单例和序列化破坏单例,那么我们就来测试一下
//测试枚举能否被反射获取对象
//enum是什么?本身也是一个Class类
public enum EnumSingle {
INSTANCE;
public EnumSingle getInstance(){
return INSTANCE;
}
}
class Test{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
//这里并不是无参构造,是一个有参数构造,且是两个,可以通过jad反编译查看
/* private EnumSingle(String s, int i) // 查看到的带参数构造,不是无参构造,注意!
{
super(s, i);
}
*/
//通过带参数构造创建对象
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
declaredConstructor.setAccessible(true); // 篡改属性
EnumSingle instance2= declaredConstructor.newInstance(); // 通过反射的方式拿到对象
EnumSingle instance=EnumSingle.INSTANCE; //通过传统的方式拿到对象
System.out.println(instance.hashCode());
System.out.println(instance2.hashCode()); //这里被捕获到了异常
//Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
}
}
为什么可以通过带参构造呢创建呢而不是默认的无参呢?是否可以通过无参构造对象呢?枚举单例是否可以被反射实例化呢?
为什么要用枚举单例,私有化构造器并不保险?接下来我们深入研究一下。
《effective java》中只简单的提了几句话:“享有特权的客户端可以借助AccessibleObject.setAccessible方法,通过反射机制调用私有构造器。如果需要低于这种攻击,可以修改构造器,让它在被要求创建第二个实例的时候抛出异常。可以发现是EnumSingleton.class.getDeclaredConstructors()获取所有构造器,会发现并没有我们所设置的无参构造器,只有一个参数为(String.class,int.class)构造器,然后看下Enum源码就明白,这两个参数是name和ordial两个属性:
//通过带参数构造创建对象
Constructor declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class,int.class);
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的构造器,结果是继续报异常。之前是因为没有无参构造器,这次拿到了父类的构造器了,说是不能够反射,我们看下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;
}
请看黄颜色标注的源码,说明反射在通过newInstance创建对象时,会检查该类是否ENUM修饰,如果是则抛出异常,反射失败。
通过实践也的得出了如下异常
Exception in thread “main” java.lang.IllegalArgumentException: Cannot reflectively create enum objects
at java.lang.reflect.Constructor.newInstance(Constructor.java:417)