java中你的单例在裸奔吗?

作者: 林锐波 | 来源:发表于2017-07-20 09:23 被阅读649次
    星爷镇楼.

    在上一篇文章java中你确定用对单例了吗?中提到单例可以被恶意的破坏,如序列化破坏和反射破坏单例的结构,好的,这个有点偏,确实在实际开发中基本也不会在意到这个问题,但是谁叫我们搞的是java,所以这个问题我们有必要知道下,这算是提高下自己的安全意识,有句古话是这样说的,居安思危嘛.

    好,请带着欢乐的心情继续往下看.

    通过反射破解单例结构

    java中你的单例是不是一直在裸奔,估计你用的是假的单例.
    我们就使用普通懒汉式来做示例吧.

    public class SingletonDemo6  implements Serializable{
        private static SingletonDemo6 s1;
      //普通懒汉式写法
        public static synchronized SingletonDemo6 getInstance() {
            if (s1 == null) {
                s1 = new SingletonDemo6();
            }
            return s1;
        }
    
    

    看看下面测试结果.
    在正常情况下,没毛病,输出结果一毛一样.

    @Test
    public  void test() throws Exception{
            SingletonDemo6 nomarlInstance1 = SingletonDemo6.getInstance();
            SingletonDemo6 nomarlInstance2 = SingletonDemo6.getInstance();
    
    //这两个单例输入的实例都是一样
    System.out.println(nomarlInstance1);
    System.out.println(nomarlInstance2);
    

    log:
    com.relice.singleton.SingletonDemo6@5a10411
    com.relice.singleton.SingletonDemo6@5a10411

    当反射遇上单例

    看下面反射破解单例的测试代码,输出两个不同的结果.

    @Test
    public  void test() throws Exception{
            SingletonDemo6 nomarlInstance1 = SingletonDemo6.getInstance();
    
             Class<SingletonDemo6> forName = (Class<SingletonDemo6>) Class
             .forName("com.relice.singleton.SingletonDemo6");
             Constructor<SingletonDemo6> c = forName.getDeclaredConstructor();
             //绕过权限管理,获取private
             c.setAccessible(true);
             //通过反射拿到`SingletonDemo6`的实例
             SingletonDemo6 reflectInsatnce = c.newInstance();
            
             // 两者的输出结是不一样的
             System.out.println(nomarlInstance1);
             System.out.println(reflectInsatnce);
    

    log:
    com.relice.singleton.SingletonDemo6@5a10411
    com.relice.singleton.SingletonDemo6@2ef1e4fa

    如何解决这种问题?

    大神说遇到问题不要急,先分析问题出现的原因.

    1. forName.getDeclaredConstructor();主要就是获取无参数构造.
    2. 也就是说通过反射拿到了私有构造方法从而再次创建实例.

    知道问题的原因那就好办了.
    我们可以在SingletonDemo6的构造方法里做判断,避免他再次创建实例.

    // 解决反射 多获取对象问题
            private SingletonDemo6() {
                if (s1 != null) {
                    try {
                        throw new RuntimeException("禁止反射获取对象");
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
    

    这样如果有人想要通过反射破坏单例结构,那就会抛出运行时异常.

    log:
    java.lang.RuntimeException: 禁止反射获取对象 at
    com.relice.singleton.SingletonDemo6.<init>(SingletonDemo6.java:24)

    通过序列化破解单例结构

    还是用SingletonDemo6来测试,通过序列化获取到实例,得出了两个不一样的结果.
    憋说话,继续看问题.

    @Test
    public  void test() throws Exception{
            SingletonDemo6 nomarlInstance1 = SingletonDemo6.getInstance();
            
        //把对象写入文件
            File file = new File(
                    "/xxx/xxx/xxx/xxx/xxx/SingletonDemo/a.txt");
            FileOutputStream fos = new FileOutputStream(file);
            ObjectOutputStream oos = new ObjectOutputStream(fos);
            oos.writeObject(nomarlInstance1);
            oos.close();
            fos.close();
            
            //序列化把对象读取
            FileInputStream fis = new FileInputStream(file);
            ObjectInputStream ois = new ObjectInputStream(fis);
            SingletonDemo6 serilizeInstance = (SingletonDemo6) ois.readObject();
            
            System.out.println(nomarlInstance1);
            System.out.println(serilizeInstance);
    }   
    

    log:
    com.relice.singleton.SingletonDemo6@68de145
    com.relice.singleton.SingletonDemo6@27fa135a

    如何解决这种问题?

    老规矩我们还是先分析.

    1. 在序列化里我们可以通过流的方式将一个对象写入内存中oos.writeObject,因此也就可以将这个对象从内存中读取出来.
    2. 但是当序列化遇到单例问题就发生了,在读取对象时jvm会重新给序列化对象分配地址.
    3. 因此我们要考虑的问题就是反序列化

    解决方法:
    当反序列化的时候:
    JVM会调用readObject方法,将我们刚刚在writeObject方法序列化好的属性,反序列化回来. 然后在readResolve方法中,我们也可以指定JVM返回我们特定的对象(不是刚刚序列化回来的对象). .该方法的分析见

    private Object readResolve() throws ObjectStreamException{
         return SingletonDemo6.s1;
    }
    

    可能我们会考虑到一个问题,就是之前的反射不是在构造方法里处理解决问题吗,那是不是序列化也可以?
    要知道序列化和反序列化,在java中是使用字节码技术生成对象,并不会执行构造器方法.

    android开发中要注意的问题

    接触过android的都知道,在其中四大组件中就有三大组件是有生命周期的,生命周期最关键的的就是context,连基本的Activty 和 Service都是从Context派生出来的,也因为这生命周期让android应用在用户体验上附上了一些生命气息,,如视频播放根据生命周期来处理播放状态;如我们想边听音乐边干些别事情,这是Service的生命周期就有帮我们做到等..

    我想说的就是.
    android组件中的生命周期是尤其重要,因此我们要善待context,在处理或者使用到组件的生命周期时也要注意规范,提高容错率.

    Android开发 单例模式导致内存泄露

    实际开发中用到最多的设计模式,如果单例设计模式认第二,我想没有敢认第一的.如工具类,application类,配置文件等.
    不扯淡了,以工具类为例,存在内存泄露问题的一些代码片段像下面这样:

    public class Util {
    
        private Context mContext;
        private static Util mInstance;
    
        private Util(Context context) {
            this.mContext = context;
        }
    
        public static Util getInstance(Context context) {
            if (mInstance == null) {
                synchronized (Util.class) {
                    if (mInstance == null) {
                        mInstance = new Util(context);
                    }
                }
            }
            return mInstance;
        }
    }
    

    其实实际开发中排查问题和定位问题一直是占据了大部分的工作时间,因此拥有一个好的开发方式可以减少很多不必要的时间浪费,这里有篇关于使用android studio检查内存泄漏的文章觉得不错.

    分析下问题:

    1. Util.getInstance(this);这个this使用的就是Activity的context.
    2. Activity的生命周期都是比较短暂的,当用户切换页面的时候基本都会把activity销毁掉,因此贯穿整个生命周期的context类也会被相应的相会.
    3. Util.getInstance(mContext);在工具类里封装一些耗时的操作也是常见的,当Activity生命周期结束,但Util类里面却还存在A的引用 (mContext),这样Activity占用的内存就一直不能回收,而Activity的对象也不会再被使用.从而造成内存泄漏.

    解决问题:

    1. 在Activity中,可以用Util.getInstance(getApplicationContext());Util.getInstance(getApplication());来代替。
      因为Application的生命周期是贯穿整个程序的,所以Util类持有它的引用,也不会造成内存泄露问题。

    2. 使用弱引用让这个引用自动被回收
      弱引用也是用来描述非必需对象的,当JVM进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。

    下面代码是使用了弱引用之后

    public class WeakRUtil {
        private static Context mContext;
        private static WeakRUtil mInstance;
    
        private WeakRUtil(Context context) {
            this.mContext=context;
        }
    
        public static WeakRUtil getInstance(Context context) {
            if (mInstance == null) {
                WeakReference<Context> actWeakRF = new WeakReference<Context>(context);
                //通过get来获取弱引用关联对象,如果为null 则就是被回收了
                mContext = actWeakRF.get();
    
                synchronized (WeakRUtil.class) {
                    if (mInstance == null) {
                        mInstance = new WeakRUtil(mContext);
                    }
                }
            }
            return mInstance;
        }
    
        public void test() {
            System.out.println("util_test");
        }
    }
    

    在java中,用java.lang.ref.WeakReference类来表示。
    当调用了System.gc(); 则即使内存足够,该引用内的数据都会被回收;

    好了,我们继续总结下:

    1. 单例的优点就是提供了对唯一实例的受控访问,减少内存分配,提高系统性能,也因为这个优点所以我们要避免单例被恶意的破坏掉了其结构.
    2. 单例在实际开发中经常使用到,而使用方法也是各种各种,为了让代码有更好的健壮性,因此一些开发中的编程习惯要养成,避免如oom异常.

    相关文章

      网友评论

        本文标题:java中你的单例在裸奔吗?

        本文链接:https://www.haomeiwen.com/subject/ptkdkxtx.html