单例模式(Singleton Pattern),Java中最简单的设计模式之一,它定义了如何在整个系统范围内仅创建只有单个实例的类。单例模式是一种创建型模式,系统中的类只有一个实例,该类负责自己创建自身的唯一单个实例,并提供一个静态方法来获取自身实例。

1. 单例模式解决的问题

单例模式(Singleton Pattern)的目的是要保证系统中一个类仅有一个实例,并且该类给外部提供一个访问它实例的方法。单例模式旨在解决系统中的类被频繁创建和销毁而占用较多资源的问题。

单例模式不允许外部创建其实例(构造器私有化),而是自身提供给外部一个静态方法来获取其单实例对象。

优点

单例类减小了资源占用,一个类仅有一个实例,内存开销小。

缺点

单例类没有接口,不能继承,与单一职责原则冲突,需要自己关注自身实例创建逻辑。

2. Java中单例模式的8种写法分析

在Java中,单例模式有8八种写法,但是可用的只有几种,我们来分析一下他们的写法和优缺点。

2.1. 饿汉式-静态常量

这种方式利用了类初始化机制,在类初始化时就创建单例实例。

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

    private AvailableEagerSingleton1() { (2)
    }

    public static AvailableEagerSingleton1 getInstance() { (3)
        return INSTANCE;
    }
}
1静态常量,类初始化时就创建实例
2构造器私有化,不允许外部直接创建实例
3提供静态方法给外部调用,以获取其实例

这种方式的优点就是实现起来简单,而且没有线程安全问题,在初始化静态属性时直接创建实例;缺点是,没有实现懒加载,如果类不会被使用,则会存在资源浪费。

如果确定类会被使用,这种方式也是推荐使用的。

2.2. 饿汉式-静态代码块

另一种懒汉式的变体是,使用静态代码块来代替静态属性创建实例,两者其实没有什么根本区别:

public class AvailableEagerSingleton2 {
    private static AvailableEagerSingleton2 INSTANCE;

    {
        INSTANCE = new AvailableEagerSingleton2(); (1)
    }

    private AvailableEagerSingleton2() {
    }

    public static AvailableEagerSingleton2 getInstance() {
        return INSTANCE;
    }
}
1静态代码块中创建实例

饿汉式最主要的区别是没有实现懒加载,可能存在内存浪费。实现懒加载,就是要把实例化的过程放到第一次获取单例类实例时(即放到静态方法获取实例时)。但是,这就会出现线程安全问题,所以解决线程安全问题是懒汉式最主要的任务。

2.3. 懒汉式-线程不安全

一个最常见的错误实现是,没有处理线程安全问题,代码如下:

public class NotAvailableLazySingleton {
    private static NotAvailableLazySingleton INSTANCE;

    private NotAvailableLazySingleton() {
    }

    public static NotAvailableLazySingleton getInstance() {
        //! 这里多线程并发时,会存在线程安全问题,可能创建多个实例,违背单例模式设计
        if (INSTANCE == null) {
            INSTANCE = new NotAvailableLazySingleton(); (1)
        }
        return INSTANCE;
    }
}
1这里,多线程时会创建多个实例,违背单例模式的设计

所以,这种模式存在线程安全问题,不可用

2.4. 懒汉式-方法加锁

那么,我们需要通过加锁来解决线程安全问题。最简单地实现懒汉式的方式就是直接在获取实例方法上加同步锁:

public class NotAvailableLockLazySingleton {
    private static NotAvailableLockLazySingleton INSTANCE;

    private NotAvailableLockLazySingleton() {
    }

    public synchronized static NotAvailableLockLazySingleton getInstance() { (1)
        //! 直接在方法上加锁,解决了线程安全问题,但是性能太低
        if (INSTANCE == null) {
            INSTANCE = new NotAvailableLockLazySingleton();
        }
        return INSTANCE;
    }
}
1直接在方法上加锁,保证线程安全性

由于每次都会在获取实例时加锁,多线程并发时性能会非常低。所以这种方式虽然可用,但是不推荐使用

2.5. 懒汉式-同步代码块

既然方法加锁性能低,那么是否可以通过同步代码块来改进呢?看代码:

public class NotAvailableLazySingleton2 {
    private static NotAvailableLazySingleton2 INSTANCE;

    private NotAvailableLazySingleton2() {
    }

    public static NotAvailableLazySingleton2 getInstance() {
        //! 这里多线程并发时,会存在线程安全问题,可能创建多个实例,违背单例模式设计
        if (INSTANCE == null) {
            // 这里并不能解决线程安全问题,因为多个线程进入这里,同样会创建多个实例
            synchronized (NotAvailableLazySingleton2.class) { (1)
                INSTANCE = new NotAvailableLazySingleton2();
            }
        }
        return INSTANCE;
    }
}
1通过同步代码块实现局部加锁

虽然通过同步代码块实现了局部加锁,但是并没有解决线程安全问题。多个线程会同时访问到 if(INSTANCE == null), 然后即使下一行代码加了锁,但是还是会创建多个实例。

所以,这种方法根本不可用

2.6. 双重检查

改进上边的同步代码块,一种简单有效的方式是进行双重检查。看代码:

public class AvailableDoubleCheckLazySingleton {
    // 注意,这里添加了volatile关键字,一旦INSTANCE修改,其他线程都能感知
    private volatile static AvailableDoubleCheckLazySingleton INSTANCE; (1)

    private AvailableDoubleCheckLazySingleton() {
    }

    public static AvailableDoubleCheckLazySingleton getInstance() {
        // 双重检查
        // 1、首先检查实例是否为空,存在多个线程同时得到INSTANCE为null的情况
        if (INSTANCE == null) { (2)
            synchronized (AvailableDoubleCheckLazySingleton.class) {
                // 2、采用同步代码块,只有一个线程能够进入,并且再次判断INSTANCE是否为null,双重检查。能够很好的解决线程安全问题
                if (INSTANCE == null) { (3)
                    INSTANCE = new AvailableDoubleCheckLazySingleton();
                }
            }
        }
        return INSTANCE;
    }
}
1首先在 INSTANCE 变量声明时使用了 volatile 关键字,保证任何一个线程修改后对其他线程都可见
2判断 INSTANCE 是否为null,不是则直接返回,保证高效
3如果 INSTANCE 为null,则进入同步代码块,然后再次判断 INSTANCE 是否为null,保证当某线程创建了实例,对其他线程可见,它们得到的是 INSTANCE != null,避免了创建多个线程

这种方式,既解决了线程安全问题,又将性损失降到最低。因此,这是推荐的一种方式。

2.7. 静态内部类

另一种实现懒加载的方式,就是利用内部类的加载机制:内部类在外部类装载时并不会立即加载,而是在使用的时候才初始化。看代码:

public class AvailableInnerClassLazySingleton {
    private AvailableInnerClassLazySingleton() {
    }

    public static AvailableInnerClassLazySingleton getInstance() {
        return InstanceHolder.INSTANCE;
    }

    private static class InstanceHolder {
        private static final AvailableInnerClassLazySingleton INSTANCE = new AvailableInnerClassLazySingleton(); (1)
    }
}
1静态内部类在外部类装载时并不会立即实例化,而是在外部类调用 InstanceHolder.INSTANCE 时,才会装载内部类,从而完成实例化

利用内部类的加载机制,完美的解决了懒加载问题,而且内部类加载时时单线程的,并不会出现线程安全问题。这种方式也是推荐的方式。

2.8. 枚举

《Effective Java》作者推荐的创建单例的方式是使用枚举类,看代码:

public enum AvailableEnumLazySingleton {
    INSTANCE;

    public void sayHello() {
        System.out.println("hello");
    }

    public static void main(String[] args) {
        AvailableEnumLazySingleton singleton1 = AvailableEnumLazySingleton.INSTANCE;
        AvailableEnumLazySingleton singleton2 = AvailableEnumLazySingleton.INSTANCE;
        System.out.println(singleton1 == singleton2);
        singleton1.sayHello();
    }
}

Java的 enum 类的枚举属性,本身是单例的,无现车安全问题而且性能也很高。利用这一点,可以很好的实现单例模式。但是,单例类可能会受到枚举类语法特性的一些限制,个人认为在实现一些工具类方法时时可行的,但是并不适合于大多数普通场景使用。

这种模式可用,但是目前使用的还是比较少。

3. 总结

典型的单例模式的应用例子是,Jdk的 Runtime 类的设计:

public class Runtime {
    private static Runtime currentRuntime = new Runtime();

    public static Runtime getRuntime() {
        return currentRuntime;
    }

    private Runtime() {}

    // ……
}

可以看到,这里使用的使静态属性的方式创建的单例。

综上,单例模式解决了频繁创建实例和销毁实例的性能开销。推荐的实现方式是支持懒加载的方式,比如:双重检查、静态内部类,枚举和静态属性的方式也是可用的。

本文示例代码见: GITHUB


相关阅读