java单例模式小记

314 阅读6分钟

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. 在类加载时便静态分配实例化对象,没有线程安全问题

缺点:

  1. 优点也即缺点,由于它加载类的时候便实例化对象,若这个对象非常大,而我们又需要加载这个类,但并没有使用到它的对象,那会占用资源.
  2. 常规手段获取没问题,但是通过反射可以破坏.

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. 不急着初始化对象,调用时才初始化

缺点:

  1. 引出了线程安全问题.
  2. 同样无法避免反射的破坏

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. 解决懒汉式带来的线程安全问题
  2. 拥有懒汉式的延迟加载对象的优点

缺点:

  1. 依旧顶不住反射的破坏.

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. 延迟加载
  2. 线程安全

缺点:

  1. 依旧会被反射破坏

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
        });
    }
}

我将不需要看的部分注释掉,会发现它和我们之前写的饿汉式很像,只不过它将对象的创建放在静态代码块中了,当类加载时,会进行静态分配.是推荐的写法 优点:

  1. 代码简洁,只需调用Single.SINGLE即可调用
  2. 天生不受反射破坏
  3. 线程安全

缺点:

  1. 和饿汉式一样,都是加载类时便创建对象,而不是调用时才创建.如果后期这个单例没有被使用,会造成资源浪费.

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. 后记 

其实除了反射还有序列化可以破坏单例的问题,第一次写博客,第一篇博客写了好久,暂时没去弄懂就先写到这.如有什么错误,烦请各位老哥指正.