单例模式之坑与爬坑

上篇简述了其中单例还有部分个人看法,本篇主要从三个问题进行开展

  1. 怎么避免线程阻塞
  2. 怎么避免内存泄漏
  3. 怎么避免被反射

首先先看一段事例代码

public class OkHttpUtils
{
    private static OkHttpClient singleton;
    private static final int TIME_OUT = 0X00000A;

    private OkHttpUtils()
    {

    }

    public static OkHttpClient getInstance(Context context) {
        synchronized (OkHttpUtils.class)
        {
            if (singleton == null)
            {
                singleton = new OkHttpClient();
                File cacheDir = new File(context.getCacheDir(), "qkl");
                int cacheSize = 10 * 1024 * 1024; // 10 MiB
                singleton.setCache(new Cache(cacheDir, cacheSize));
                singleton.setConnectTimeout(TIME_OUT, TimeUnit.SECONDS);
                singleton.setReadTimeout(TIME_OUT, TimeUnit.SECONDS);
            }
            return singleton;
        }
    }
}

这是之前看到的一段代码,这是一个基于okhttp2.x的一个封装。上述代码,有的会把synchronized 加在方法上,其实两种效果一样。通过这段代码,逐步分析上述所说的三个问题。

首先,当同时有多个线程一起访问这个方法的时候因为有锁会一个个执行,未被执行的线程就会被锁在方法外,当并发量过大的时候,如果方法内部是一个很耗时的操作,这就造成了线程阻塞。这就如同过独木桥,对于独木桥的承载,一次只能承载一个人,如果这独木桥很长,那后面的人等待的时间就会很长,便会一直阻塞在独木桥的一端。

对于内存泄漏,有一个很常见的问题,如果一个类中,只要有一个字段被长期占有得不到释放,那么这个类就得不到及时销毁。上述代码中的context就是,当第一个context传过来的时候,这个context被会被长期占有,,如果此类使用足够多的强引用,再点击一些其它页面,很容易产生OOM(OOM主要来源于强引用类型,就算发生OOM,强引用类型的对象也不会被销毁)。在okhttp3.x版本中,对于2.x版本,源码做了很大的改变和调整,而且,不管在2.x版本或是3.x版本中,如果没有特殊需要,不用刻意去配置缓存和空间,源码中都有默认的设置,如果真的想设置缓存路径,可以在application中设置,在上述单例过程中直接传过来,而并不需要传一个context。

public class OkHttpUtils
{
    private static OkHttpClient singleton;
    private static final int TIME_OUT = 0X00000A;

    private OkHttpUtils()
    {

    }

    public static OkHttpClient getInstance() {
        if (singleton == null)
        {
            synchronized (OkHttpUtils.class)
            {
                if (singleton == null)
                {
                    singleton = new OkHttpClient();
                    int cacheSize = 10 * 1024 * 1024; // 10 MiB
                    singleton.setCache(new Cache(ZeroApplication.cacheDir,
                            cacheSize));
                    singleton.setConnectTimeout(TIME_OUT, TimeUnit.SECONDS);
                    singleton.setReadTimeout(TIME_OUT, TimeUnit.SECONDS);
                }
            }
            return singleton;
        }
    }
}

改成这样,看起来好像是完美了,有的人会疑问,为什么上锁后的执行代码中还要一次判空操作,这是防止多个线程在singleton 判断为空的过程中都已通过执行,被锁在外面,当一个个执行锁内方法的时候,多次实例化。这个貌似一切顺理成章的代码,背后却有依然还有一个大问题。

最后一个问题,这是我在一次面试过程中遇到的,面试官问我怎么解决通过反射机制拿到私有构造器,其实这个问题之前在《effective Java》上看到过,当时没怎么在意,今天再次翻阅书籍的时候,看到上面写道,“享有特权的客户端可以借助AccessibleObject.setAccessible方法,通过反射机制调用私有构造器。如果需要抵御这种攻击,可以修改构造器,让它在被要求创建第二个实例的时候抛出异常。”对于反射机制调用私有构造器,之前和别人讨论过,有的说在私有构造器中加入一些没必要的字段,我擦,这方法虽然行之有效,但也太傻了吧,有的说如果别人想通过反射去拿,那就让它拿呗,不用管的,一般情况下,大家都是这么做的,这是一个普遍方式,还有的说用抛异常的方式,还有的说用枚举,最后两种方式都是可取的,代码如下:

/**
 * created by zero on 2016-07-05
 */
public class SingletonTest
{
    public static void main(String[] args) {
         try
            {
                Class classType = Singleton2.class;
                Constructor c = classType.getDeclaredConstructor();
                c.setAccessible(true);
                Singleton2 s1 = (Singleton2)c.newInstance();
                System.out.println(s1.test());
                Singleton2 s2 = Singleton2.getInstance();
                System.out.println(s2.test());
            }
            catch (Exception e)
            {
                e.printStackTrace();
            }
    }
}
/**
 * created by zero on 2016-07-05
 */
public class Singleton2
{
    private static boolean flag = false;

    private Singleton2()
    {
        synchronized (Singleton2.class)
        {
            if (flag == false)
            {
                flag = !flag;
            } else
            {
                throw new RuntimeException("单例模式被侵犯!");
            }
        }
    }

    private static class SingletonHolder
    {
        private static final Singleton2 INSTANCE = new Singleton2();
    }

    public static Singleton2 getInstance() {
        return SingletonHolder.INSTANCE;
    }

    public String test(){
        return "test";
    }
}
test
Exception in thread "main" java.lang.ExceptionInInitializerError
    at com.zm.zero.test.Singleton2.getInstance(Singleton2.java:29)
    at com.zm.zero.test.SingletonTest.main(SingletonTest.java:15)
Caused by: java.lang.RuntimeException: 单例模式被侵犯!
    at com.zm.zero.test.Singleton2.(Singleton2.java:18)
    at com.zm.zero.test.Singleton2.(Singleton2.java:9)
    at com.zm.zero.test.Singleton2$SingletonHolder.(Singleton2.java:25)
    ... 2 more

此处可以看出,第一次用反射成功调用了test,第二次创建实例的时候便会抛出异常。在1.5以后,《effective Java》提倡枚举,示例如下:

/**
 * created by zero on 2016-07-05
 */
public class SingletonTest
{
    public static void main(String[] args) {
        try
        {
            Class classType = Singleton3.class;
            Constructor c = (Constructor) classType
                    .getDeclaredConstructor();
            c.setAccessible(true);
            c.newInstance();
        } catch (Exception e)
        {
            e.printStackTrace();
        }
    }
}
public enum Singleton3
{
    INSTANCE;

    public void test()
    {
        System.out.println("test");
    }
}
java.lang.NoSuchMethodException: com.zm.zero.test.Singleton3.()
    at java.lang.Class.getConstructor0(Unknown Source)
    at java.lang.Class.getDeclaredConstructor(Unknown Source)
    at com.zm.zero.test.SingletonTest.main(SingletonTest.java:15)

由此可见,这种写法也可以防止单例模式被攻击,这种写法在功能上和共有域方法相近,但是它更加简洁,无偿地提供了序列化机制,绝对防止多次实例化,即使在面对复杂的序列化或者反射攻击的时候。虽然这种方法还没有广泛采用,但是单元素的枚举类型已经成为Singleton的最佳是方法。

你可能感兴趣的:(设计模式,设计模式与日常开发)