上篇简述了其中单例还有部分个人看法,本篇主要从三个问题进行开展
首先先看一段事例代码
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的最佳是方法。