Java单例模式的8种写法

6,128 阅读5分钟

这是我参与8月更文挑战的第3天,活动详情查看:8月更文挑战

单例模式是设计模式之一,同时也是Java面试中的高频考点,这一篇来总结一下Java单例模式的各种写法。

单例模式介绍

单例模式的作用:

  • 节省内存和计算,实例只创建一次,不必重复创建
  • 保证结果正确,比如单例计数器,用作多线程的数据统计
  • 方便管理,例如日期工具类,字符串工具类,不需要创建那么多的实例

单例模式适用场景:

  • 无状态的工具类:比如日志工具类,不管是在哪里适用,我们需要的只是它帮我们记录日志信息,除此之外,并不需要在它的实例对象上存储任何状态,这时候我们就只需要一个实例对象即可。
  • 全局信息类:比如我们在一个类上记录网站的访问次数,我们不希望有的访问被记录在对象A上,有的却记录在对象B上,这时候我们就让这个类成为单例。

单例模式的8种写法

饿汉式——静态常量(可用)

public class Singleton1 {
    private final static Singleton1 INSTANCE = new Singleton1();

    private Singleton1() {

    }

    public static Singleton1 getInstance() {
        return INSTANCE;
    }
}

优点:这种写法比较简单,就是在类装载的时候就完成实例化,避免了线程同步问题。

缺点:在类装载的时候就完成实例化,没有达到lazy loading的效果,如果从始至终都未使用过这个实例,则会造成内存的浪费

饿汉式——静态代码块(可用)

public class Singleton2 {

    private final static Singleton2 INSTANCE;

    static {
        INSTANCE = new Singleton2();
    }

    private Singleton2() {
    }

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

这种方法和上面的方法类似,只不过将类实例化的过程放在了静态代码块中,也是在类装载的时候,就执行静态代码块中的代码,初始化类的实例。优缺点和上面是一样的。

懒汉式——线程不安全(不可用)

public class Singleton3 {
    private static Singleton3 instance;

    private Singleton3() {

    }

    public static Singleton3 getInstance() {
        if (instance == null) {
            instance = new Singleton3();
        }
        return instance;
    }
}

这种写法起到了lazy loading的效果,但是只能在单线程下使用。如果在多线程下,一个线程进入了if(singleton==null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会多次创建实例。所以在多线程环境下不可使用这种方式。

懒汉式——线程安全,同步方法(不推荐用)

public class Singleton4 {
    private static Singleton4 instance;

    private Singleton4() {

    }

    public synchronized static Singleton4 getInstance() {
        if (instance == null) {
            instance = new Singleton4();
        }
        return instance;
    }
}

解决上面第三种方式的线程不安全问题,做个线程同步就可以了,于是就对getInstance()方法进行了线程同步。

缺点:效率太低了,每个线程在想获得类的实例的时候,执行getInstance()方法都要进行同步。而其实这个方法只执行一次实例化代码就够了,后面的想获得该类实例,直接return就行了。方法进行同步效率太低要改进。

懒汉式——线程不安全,同步代码块(不推荐)

public class Singleton5 {
    private static Singleton5 instance;

    private Singleton5() {

    }

    public static Singleton5 getInstance() {
        if (instance == null) {
            synchronized (Singleton5.class) {
                instance = new Singleton5();
            }
        }
        return instance;
    }
}

由于第四种实现方式同步效率太低,所以摒弃同步方法,改为同步产生实例化的代码块。但是这种同步并不能起到线程同步的作用。跟第3种实现方式遇到的情形一致,加入一个线程进入到了if(singleton==null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这是便会产生多个实例。

双重检查(推荐面试用)

public class Singleton6 {
    private volatile static Singleton6 instance;

    private Singleton6() {

    }

    public static Singleton6 getInstance() {
        if (instance == null) {
            synchronized (Singleton6.class) {
                if (instance == null) {
                    instance = new Singleton6();
                }
            }
        }
        return instance;
    }
}

优点:线程安全,延迟加载,效率较高

双重检查的好处:保证线程安全和性能

volatile作用:创建对象不是原子操作,要防止重排序

静态内部类(可用)

public class Singleton7 {
    private Singleton7() {
    }

    private static class SingletonInstance {

        private static final Singleton7 INSTANCE = new Singleton7();
    }

    public static Singleton7 getInstance() {
        return SingletonInstance.INSTANCE;
    }
}

静态内部类在Singleton类被装载时不会立即实例化,而是在需要实例化时,调用getInstance方法,才会装载SingletonInstance类,从而完成Singleton的实例化。

类的静态属性只会在第一次加载类的时候初始化,JVM帮助我们保证了线程的安全性,在类进行初始化时,别的线程无法进入。

优点:避免了线程不安全,延迟加载,效率高。

枚举(推荐用)

public enum  Singleton8 {
    INSTANCE;

    public void getInstance() {

    }
}

不仅能避免多线程同步问题,还是懒加载,而且还能防止反序列化重新创建新的对象。

不同写法的对比

  • 饿汉:简单,但是没有lazy loading
  • 懒汉:有线程安全问题
  • 静态内部类:可用
  • 双重检查:面试用
  • 枚举:最好

各种写法的适用场合

  • 最好的方法是利用枚举,因为还可以防止反序列化重新创建新的对象
  • 非线程同步的方法不能使用
  • 如果程序一开始要加载的资源太多,那么就应该使用懒加载
  • 饿汉式如果是对象的创建需要配置文件就不适用
  • 懒加载虽然好,但是静态内部类这种方式会引入编程复杂性,大部分情况还是推荐枚举,简单。