1. 理解单例模式
单例模式,如何保证一个类只有一个实例,作为最常用的设计模式之一,使用广泛.在java中,目前常见的5种写法.
1.1. 饿汉式
很容易想到,将构造方法设置为私有,设置一个静态私有属性创建对象,然后提供一个public的静态方法返回引用.外部获取对象常规手段只能调用我们提供的getSingle()方法
public class Single {
private static Single single=new Single();
private Single(){
}
public static Single getSingle(){
return single;
}
}
优点:
- 在类加载时便静态分配实例化对象,没有线程安全问题
缺点:
- 优点也即缺点,由于它加载类的时候便实例化对象,若这个对象非常大,而我们又需要加载这个类,但并没有使用到它的对象,那会占用资源.
- 常规手段获取没问题,但是通过反射可以破坏.
1.2. 懒汉式
为了上面饿汉式的第一个缺点,那么我们便延迟这个对象的创建,当调用的时候再创建对象.
public class Single {
private static Single single;
private Single(){}
public static Single getSingle(){
if(single==null){
single=new Single();
}
return single;
}
第一次调用静态方法时,创建对象,以后调用直接返回对象即可,这样就没有懒汉式的第一个缺点了. 可是问题来了,这个写法又带来了新的问题,那便是线程安全问题.
假设线程A和线程B同时打算获取单例,首先A分配到时间片,A先调用getSingle方法到达判断得到sigle为null,满足if条件.就在这时候恰巧时间片到了,轮到B执行,B也判断到sigle为null,满足if条件. B去创建对象并获取到新对象,A也一样. 那么AB将会得到两个不同的对象.
优点:
- 不急着初始化对象,调用时才初始化
缺点:
- 引出了线程安全问题.
- 同样无法避免反射的破坏
1.3. 双重检验模式
既然懒汉式有线程安全问题,那么现在如何解决,最简单的思路,直接给getInstance()静态方法加一把同步锁.
public synchronized static Single getSingle(){
if(single==null){
single=new Single();
}
return single;
}
思路没错,但是有点小问题,第一个线程进来创建单例后,后面的线程进来每次都要做判断,而且后面进来的都只是获取不存在线程安全问题,效率比较低.为了提高效率便出现了双重检验模式.
public class Single {
private static Single single;
private Single(){}
public static Single getSingle() {
if (single == null) {
synchronized (Single.class) {
if (single == null) {
single = new Single();
}
}
}
return single;
}
}
第一个线程进来后进入同步块创建对象,后续进来的线程不需要进入同步块.但其实还有一点小问题,问题出在创建对象这个过程其实是分为好几条指令,为对象分配空间,初始化,返回对象引用等一系列操作,而jvm有时候可能会将指令进行重排,就会出现问题,假设下面的情况:
第一个线程进来创建对象,虚拟机为这个对象分配内存,然后直接就返回了这个对象的引用(指令重排,本来应该先初始化对象),这时候恰巧轮到B线程进来,得到对象不为空,直接就获取到了对象进行使用,但是这个对象里面的内容还没有初始化成功,这时候就出问题了.
解决办法就是:使用volatile关键字修饰静态变量,它重要作用是禁止指令重排.完整代码如下
public class Single {
private volatile static Single single;
private Single(){}
public static Single getSingle() {
if (single == null) {
synchronized (Single.class) {
if (single == null) {
single = new Single();
}
}
}
return single;
}
}
优点:
- 解决懒汉式带来的线程安全问题
- 拥有懒汉式的延迟加载对象的优点
缺点:
- 依旧顶不住反射的破坏.
1.4. 静态内部类
通过静态内部类实现单例.外部类Single加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存,虚拟机会保证Single对象的创建是线程安全的.
public class Single{
private Single(){}
private static class Inner{
private static Single single=new Single();
}
public static Single getInstance(){
return Inner.single;
}
}
优点:
- 延迟加载
- 线程安全
缺点:
- 依旧会被反射破坏
1.5. 枚举单例
上述的几种单例模式都有一个缺点没搞定,那就是会受到反射的破坏.枚举模式不受反射干扰.而且枚举单例的代码非常少!
public enum Single {
SINGLE;
}
我们试着用jad反编译看看为什么它是单例的.
public final class Single extends Enum
{
// public static Single[] values()
// {
// return (Single[])$VALUES.clone();
// }
// public static Single valueOf(String name)
// {
// return (Single)Enum.valueOf(Single, name);
// }
private Single(String s, int i)
{
super(s, i);
}
public static final Single SINGLE;
private static final Single $VALUES[];
static
{
SINGLE = new Single("SINGLE", 0);
$VALUES = (new Single[] {
SINGLE
});
}
}
我将不需要看的部分注释掉,会发现它和我们之前写的饿汉式很像,只不过它将对象的创建放在静态代码块中了,当类加载时,会进行静态分配.是推荐的写法 优点:
- 代码简洁,只需调用Single.SINGLE即可调用
- 天生不受反射破坏
- 线程安全
缺点:
- 和饿汉式一样,都是加载类时便创建对象,而不是调用时才创建.如果后期这个单例没有被使用,会造成资源浪费.
2. 反射的破坏
前面总提到反射破坏单例,如何破坏呢,下面演示破坏双重检测单例的例子,其他的类似.
public static void main(String[] args) {
Class<Single> s = Single.class;
try {
Constructor<Single> de = s.getDeclaredConstructor(); //获取所有的构造器
de.setAccessible(true); //设置允许访问private构造器
Single single1 = de.newInstance();
Single single2 = de.newInstance();
System.out.println(single1==single2);
System.out.println(Single.getSingle()==single1);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
输出结果: false false
将Single换成枚举单例,反射将会报异常,就不贴代码了都是类似的操作.网上有说通过定义一个变量判断s会否第一次创建对象,下次创建对象就直接返回对象,这样反射就不能创建两次对象了.但是好像也没什么用,通过反射依然可以更改变量的值绕开限制.
3. 后记
其实除了反射还有序列化可以破坏单例的问题,第一次写博客,第一篇博客写了好久,暂时没去弄懂就先写到这.如有什么错误,烦请各位老哥指正.