关于java单例的深入思考

整理下之前的学习笔记,发现在工作中虽然经常用到单例,但都没有怎么去深究其对与错,因为我们都是老实的按照正确的姿势去获取单例,正好翻到这个笔记,回顾下(其实要实现一个真正的单例还是比较复杂的)。

单例模式应该是最简单也是最常用的一种设计模式了,个人认为对于一个单例来说,基本的几个特性必须满足:
1,必须是单例!!!这点很重要,不能是个伪单例
2,能够延迟加载,如果程序中一直没有用到,则不需要new对象
3,必须是线程安全的,既然是单例了,全局唯一(jvm级别),那必须满足线程安全
4,没有性能问题,获取单例时最好不能有类似锁的东东

所以对于那些不满足上面条件的单例写法这里就不说了,我们首先来看一种满足上面条件的单例:

public class Singleton {
    private static class SingletonHolder{
        private static final Singleton instance = new Singleton();
    }
    private Singleton(){
    }

    public static Singleton getInstance(){
        return SingletonHolder.instance;
    }
}

看上去貌似条件都满足,但我们不禁要说,它不满足第一条,他其实是个伪单例,看下面测试例子:

public static void main(String[] args) throws Exception{
        Singleton singleton = Singleton.getInstance();
        System.out.println("1:"+singleton);
        Class cls = Class.forName(Singleton.class.getName());
        Constructor constructor = cls.getDeclaredConstructor(new Class[0]);
        constructor.setAccessible(true);
        singleton = (Singleton) constructor.newInstance(null);
        System.out.println("2:"+singleton);
    }

输出结果如下:

1:com.Singleton@1d44bcfa
2:com.Singleton@266474c2

看到了么,通过反射可以获取新的对象,它是一个伪单例!!!为此有人提出了个解决办法,用一个标记标识是否已经访问过私有构造函数了,如果发现已经访问过,则表示当前正在试图重新创建对象,抛出异常:

public class Singleton {
    private static volatile boolean flag = false;
    private static class SingletonHolder{
        private static final Singleton instance = new Singleton();
    }
    private Singleton(){
        synchronized (Singleton.class){
            if(!flag){
                flag = !flag;
            }else{
                throw new RuntimeException("can not create object");
            }
        }
    }

    public static Singleton getInstance(){
        return SingletonHolder.instance;
    }
}

但是这个也不顶用,别忘了变量flag我们照样可以通过反射修改其值,如下:

public static void main(String[] args) throws Exception{
        Singleton singleton = Singleton.getInstance();
        System.out.println("1:"+singleton);
        Class cls = Class.forName(Singleton.class.getName());
        Constructor constructor = cls.getDeclaredConstructor(new Class[0]);
        constructor.setAccessible(true);
        Field field = Singleton.class.getDeclaredField("flag");
        field.set(Singleton.class,false);
        singleton = (Singleton) constructor.newInstance(null);
        System.out.println("2:"+singleton);
    }

同样可以创建一个新的对象,所以这个办法不可取!

上面出问题的原因在于反射,那我们是否可以通过禁止反射来达到我们的目的呢?看到一种解决办法说使用SecurityManager,禁止反射访问私有函数,看下面:

public class Singleton {
    static {
        System.setSecurityManager(new SecurityManager());
    }
    private static class SingletonHolder{
        private static final Singleton instance = new Singleton();
    }
    private Singleton(){
    }

    public static Singleton getInstance(){
        return SingletonHolder.instance;
    }
}

看下测试后的输出:

1:com.Singleton@1d44bcfa
Exception in thread "main" java.security.AccessControlException: access denied ("java.lang.reflect.ReflectPermission" "suppressAccessChecks")
	at java.security.AccessControlContext.checkPermission(AccessControlContext.java:472)
	at java.security.AccessController.checkPermission(AccessController.java:884)
	at java.lang.SecurityManager.checkPermission(SecurityManager.java:549)
	at java.lang.reflect.AccessibleObject.setAccessible(AccessibleObject.java:128)
	at com.Singleton.main(Singleton.java:23)

找了好些案例来测试,采用这种方式貌似确实可以起到限制作用了。但是,一个更大的坑出现了,System.setSecurityManager(new SecurityManager());这行代码同时也禁止了其他类进行反射操作,这是一干子打死啊,坑吧!

回过头来,我们再思考下,难道就真的没有办法实现仅针对这个类的单例实现的限制了吗?我们再次分析下代码,可以发现,通过正规方式获取单例和通过反射获取单例,所经过的路径是不一样的。由之前的文章我们可以知道,函数在执行的过程中,都会有相应的栈帧记录调用过程中的某些数据,根据这个启发,我们只需要检测所有的栈帧上是否包含了正确获取单例的方式来阻止反射创建新的对象!首先我们改下私有构造函数,还是最开始的测试程序,看下两种方式所经历的栈是怎样的:

private Singleton() {
        StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
        for(StackTraceElement stackTraceElement : stackTraceElements){
            System.out.println(stackTraceElement.toString());
        }
    }

输出结果如下:

java.lang.Thread.getStackTrace(Thread.java:1589)
com.Singleton.<init>(Singleton.java:15)
com.Singleton.<init>(Singleton.java:9)
com.Singleton$SingletonHolder.<clinit>(Singleton.java:11)
com.Singleton.getInstance(Singleton.java:22)
com.Singleton.main(Singleton.java:26)
1:com.Singleton@a6a21a
===============================================================================
java.lang.Thread.getStackTrace(Thread.java:1589)
com.Singleton.<init>(Singleton.java:15)
sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:57)
sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
java.lang.reflect.Constructor.newInstance(Constructor.java:526)
com.Singleton.main(Singleton.java:32)
2:com.Singleton@1dc049d

看到了吧,正确获取单例的姿势为com.Singleton.getInstance(Singleton.java:22)。ok,有了这个前提,我们改造下这个单例:

public class Singleton {

    private static final String SINGLETON_CORRECT_PATH="com.Singleton.getInstance(Singleton.java";//直接写死

    private static class SingletonHolder {
        private static final Singleton instance = new Singleton();
    }

    private Singleton() {
        boolean flag = false;
        StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
        for(StackTraceElement stackTraceElement : stackTraceElements){
            String str = stackTraceElement.toString();
            if(str.contains(SINGLETON_CORRECT_PATH)){
                flag = true;
                break;
            }
        }
        if(!flag){
            throw new RuntimeException("not correct way to get singleton");
        }
    }
    public static Singleton getInstance() {
        return SingletonHolder.instance;
    }
}

ok,这样就可以解决反射的问题了。但是,我们还得思考下,是否还有其他途径可以再创建一个Singleton,我们知道java中创建对象的方式一共大概有5种(应该没有遗漏了吧):

1,通过关键字new产生对象

2,使用Class.newInstance创建对象

3,使用Constructor.newInstance创建对象

4,通过clone创建对象

5,通过反序列化创建对象

其中前面3种都调用了构造函数,后面两种没有调用构造函数,后面两种方式可能破坏单例。所以我们的单例类不能实现Cloneable接口,也不能实现Serializable接口。

看到了吧,单例看似简单,但是实际上比较复杂,为此,我们不禁要问,没有其他的方式实现单例吗?答案是有的,那就是使用枚举(effective java 作者推荐使用这种方式):

public enum  Singleton {
    INSTANCE;
	//下面是各种函数
}

为什么说它能是个单例呢,java spec文档中有这样一段话:

It is a compile-time error to attempt to explicitly instantiate an enum type (§15.9.1). The final clone method in Enum ensures that enum constants can never be cloned, and the special treatment by the serialization mechanism ensures that duplicate instances are never created as a result of deserialization. Reflective instantiation of enum types is prohibited. Together, these four things ensure that no instances of an enum type exist beyond those defined by the enum constants.

其中关于反射的说法我们来验证下:

public static void main(String[] args) throws Exception{
        Class cls = Class.forName(Singleton.class.getName());
        Constructor[] constructors = cls.getDeclaredConstructors();
        Constructor constructor = constructors[0];
        Singleton s = (Singleton) constructor.newInstance("",0);
        System.out.println(s);
    }

输出结果如下:

Exception in thread "main" java.lang.IllegalArgumentException: Cannot reflectively create enum objects
	at java.lang.reflect.Constructor.newInstance(Constructor.java:417)
	at com.Singleton.main(Singleton.java:16)

不能通过反射创建枚举对象,这样就没有入口再此创建对象了!不过这是为什么呢?查看newInstance方法,我们发现有这一行:

看到了,反射创建对象时有这个限制,不能通过枚举创建对象。仔细分析上面的代码,发现需要满足条件一定的条件才会走到这里,那我们有办法突破这个限制吗?首先我们先javac编译下这个枚举,为了直观,我们用jad反编译下,得到如下代码:

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.

public final class Singleton extends Enum {

	public static final Singleton INSTANCE;
	private static final Singleton $VALUES[];

	public static Singleton[] values() {
		return (Singleton[]) $VALUES.clone();
	}

	public static Singleton valueOf(String s) {
		return (Singleton) Enum.valueOf(Singleton, s);
	}

	private Singleton(String s, int i) {
		super(s, i);
	}

	static {
		INSTANCE = new Singleton("INSTANCE", 0);
		$VALUES = (new Singleton[]{INSTANCE});
	}
}

好了,有了这个这个直观的代码后,我们编写下面的代码:

public static void main(String[] args) throws Exception{
        Class<Singleton> cls = Singleton.class;
        Constructor constructor = cls.getDeclaredConstructors()[0];
        constructor.setAccessible(true);
        //下面的代码与constructor.newInstance的作用是一样的,不同的是绕过了枚举的一些校验
        Field constructorAccessorField = Constructor.class.getDeclaredField("constructorAccessor");
        constructorAccessorField.setAccessible(true);
        ConstructorAccessor ca = (ConstructorAccessor) constructorAccessorField.get(constructor);
        if(ca == null){
            Method method = Constructor.class.getDeclaredMethod("acquireConstructorAccessor");
            method.setAccessible(true);
            ca = (ConstructorAccessor)method.invoke(constructor);
        }
        Singleton enumValue = (Singleton)ca.newInstance(new Object[]{"INSTANCE2",1});


        Field valuesField = Singleton.class.getDeclaredField("$VALUES");
        valuesField.setAccessible(true);
        Field modifiersField = Field.class.getDeclaredField("modifiers");
        modifiersField.setAccessible(true);
        modifiersField.setInt(valuesField,valuesField.getModifiers()&~Modifier.FINAL);
        Singleton[] oldValues = (Singleton[])valuesField.get(null);
        Singleton[] newValues = new Singleton[oldValues.length+1];
        System.arraycopy(oldValues,0,newValues,0,oldValues.length);
        newValues[oldValues.length] = enumValue;
        valuesField.set(null,newValues);
        for(Singleton s : Singleton.values()){
            System.out.println(s);
        }
    }

输出结果如下:

INSTANCE
INSTANCE2

看到了吧,枚举实现的单例还是会被突破的!!!!不得不感叹java反射的强大!!!如何对这种情况进行限制呢,还是采用上面所说的方法根据stackTrace进行判断,更改下代码:

private Singleton(){
        String currentSingletonPath = "com.Singleton.<clinit>(Singleton.java";//直接写死
        boolean flag = false;
        StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
        for(StackTraceElement stackTraceElement : stackTraceElements){
            String str = stackTraceElement.toString();
            if(str.contains(currentSingletonPath)){
                flag = true;
                break;
            }
        }
        if(!flag){
            throw new RuntimeException("not correct way to get singleton");
        }
    }

再次用上面的"绕过check"的代码测试,发现会抛出异常,我们的目的看似达到了,但是,我们似乎还忘记了一个超级工具:Unsafe,通过allocateInstance照样可以突破单例实现,我们可以采用一个简单的办法对此做限制,比如push代码的时候或者发布应用的时候check下java文件中是否引入了Unsafe类(可以分级别或者对特定的应用进行check),如果引入了,拒绝push或发布,另一个办法可能就得修改jvm的实现了,这块等我研究下jvm代码后再来补充。

不得不说,一个简单的单例,深究起来竟然这么复杂,最后我们总结下:

经过上面的代码,我们知道一个单例应该有如下特性:

1,必须是单例!!!这点很重要,不能是个伪单例
2,能够延迟加载,如果程序中一直没有用到,则不需要new对象
3,必须是线程安全的,既然是单例了,全局唯一(jvm级别),那必须满足线程安全
4,没有性能问题,获取单例时最好不能有类似锁的东东
5,防止序列化产生新对象
6,防止反射攻击

那么通过class方式产生的单例和enum方式产生的单例有什么区别呢,个人认为,如果采用enum方式,那么你的属性就不大好配置,尤其是属性很多的时候。

最后,我想说的是,在实际工作中,我们很少去做文章中那样的限制,因为我们都是规矩的去获取单例,但是当你的应用对使用单例有安全性要求时,就不得不考虑了,同时作为一个技术人员应该对自己写的程序应该有严苛的要求,需要思考下破坏单例是否会对你的程序造成不可恢复的灾难!

发布了42 篇原创文章 · 获赞 25 · 访问量 11万+
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 大白 设计师: CSDN官方博客

分享到微信朋友圈

×

扫一扫,手机浏览