单例模式那件小事,看了你不会后悔

Java基础

浏览数:673

2019-5-10

  欢迎关注下文:单例模式不是一件小事,快回来看看。

  单例模式是一种创建型模式,某个类采用单例模式,则在这个类被创建后,只可能产生一个实例供外部访问,并且提供一个全局的访问点。

  主要思想如下:

  • 将构造方法私有化( 声明为 private ),这样外界不能随意 new 出新的实例对象;
  • 声明一个私有的静态的实例对象,供外界使用;
  • 提供一个公开的方法,让外界获得该类的实例对象。

  具体实现代码如下:

  代码①

public class Singleton {
    /**
     * 构造方法私有化
     */
    private Singleton() {
    }

    /**
     * 定义一个私有的静态的实例
     */
    private static Singleton sSingleton = new Singleton();

    /**
     * 提供静态的方法给外界访问
     * 
     * @return
     */
    public static Singleton getInstance() {
        return sSingleton;
    }
}

  上面代码①便是传说中的饿汉式单例模式。饿汉式有一个缺点是在类一加载的时候,就实例化,提前占用了系统资源。为此,我们可以稍微优化一下:

  代码②

public class Singleton {
    private Singleton() {
    }
private static Singleton sSingleton; public static Singleton getInstance() { if (sSingleton == null) { sSingleton = new Singleton(); } return sSingleton; } }

  这样便成了“懒人”的使用方式,在需要使用该类的时候,再实例化,解决了上面所说的缺点,称为“懒汉式”。不过,这样存在线程安全问题,可能会造成重复创建对象,与单例模式的思想相悖。所以我们需要改进该方法:

  代码③

public class Singleton {
    private Singleton() {
    }

    private static Singleton sSingleton;

    public synchronized static Singleton getInstance() {
        if (sSingleton == null) {
            sSingleton = new Singleton();
        }
        return sSingleton;
    }
}

  OK,给方法加上同步,就可以避免并发环境下的问题了。这就是传说中的懒汉式单例模式。同步该方法后,可以避免多个线程重复创建对象,因为每次只能有一个线程去访问该方法,其他线程必须等待,假设现在实例对象已经初始化了,不需要再创建了,上面的方式就显得效率低了。所以,该方法可以继续做如下优化:

  代码④

public class Singleton {
    private Singleton() {
    }

    /**
     * volatile is since JDK5
     */
    private static volatile Singleton sSingleton;

    public static Singleton getInstance() {
        if (sSingleton == null) {
            synchronized (Singleton.class) {
                // 未初始化,则初始instance变量
                if (sSingleton == null) {
                    sSingleton = new Singleton();
                }
            }
        }
        return sSingleton;
    }
}

  上面的代码,不再同步方法了,采用双重判断,同步代码块,这样大大地提高了懒汉式单例模式的效率,特别要注意的是:声明对象的时候,使用了一个关键字:volatile,该关键字是从JDK1.5之后新加入的,不加该关键字,编译器可能会失去大量优化的机会或者可能会在编译时出现一些不可预知的错误。

  此外,还有一种静态内部类实现单例模式的方法,如下:

  代码⑤

public class Singleton {

    private Singleton () {
    }

    private static class InnerClassSingleton {
     private final static Singleton sSingleton = new Singleton(); } public static Singleton getInstance() { return InnerClassSingleton.sSingleton; } }

   该方法简单明了,不需要同步,代码也不是很复杂。上面代码中静态内部类 InnerClassSingleton 在 Singleton 类加载的时候并不会加载,下面修改代码④,进行验证:

public class Singleton {

    public Singleton () {
    }

    private static class InnerClassSingleton {
        static{
            System.out.println("-----InnerClassSingleton 已经加载了....");
        }
        private final static Singleton sSingleton = new Singleton();
    }

    public static Singleton getInstance() {
        System.out.println(InnerClassSingleton.sSingleton.hashCode());
        return InnerClassSingleton.sSingleton;
    }
}

  注意特意将 Singleton 的构造方法公开化了,再编写一个测试类:

public class Test {
    public static void main(String[] args) {
        new Singleton();
        System.out.println(Singleton.class);
    }
}

  运行代码,打印结果只有一行:

class com.examle.joy.Singleton

  这说明静态内部类并没有加载,再次修改测试类:

public class Test {
    public static void main(String[] args) {
        System.out.println(Singleton.getInstance());
        System.out.println(Singleton.getInstance());
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Singleton.getInstance());
            }
        }).start();
    }
}

  这次的运行结果如下:

-----InnerClassSingleton 已经加载了...
com.examle.joy.Singleton@203b4f0e
com.examle.joy.Singleton@203b4f0e
com.examle.joy.Singleton@203b4f0e

  上面的打印结果是符合我们预期的。

  最后总结一下:代码⑤是单例模式最佳的实现方法,在实际开发中,饿汉式代码简洁,容易理解,用的比较多,如果不涉及并发操作的话,也可以使用懒汉式代码②,设计到多线程并发问题,用懒汉式的话,需要使用代码④,代码⑤显得很优雅,是个人比较推荐的一种方式。最后值得一提的是,所有的单例模式,都只能用在非反射场景中,因为利用反射,成员变量或者方法即便声明为 private,也可以被外部访问。

作者:SharpCJ