单例模式虽然简单但很重要,在面试中也是高频考点,需要重点理解,灵活运用,特别是每种实现方式的优缺点以及演进之路。
概述
单例模式:私有化构造器,确保一个类只有一个实例,并提供该实例的全局访问点。对于一些需要频繁创建销毁的对象,使用单例模式可以节省系统资源,提供系统性能。属于创建型设计模式的一种。
案例场景
- 数据库连接池
- JDK: java.lang.Runtime#getRuntime()
实现方式
饿汉式(线程安全)
public class Singleton_00 {
private Singleton_00(){};
// 在类加载时就实例化完成
public static Singleton_00 singleton_00 = new Singleton_00();
}
- JVM启动时会加载所有类,这种方式在类加载时就进行了实例化,后续对外提供访问权限,因此是线程安全的。
- 相对于延迟加载的方式,我们称这种单例实现方式为饿汉式。优点是实现简单,线程安全。缺点就是类加载时实例化会先消耗内存空间,浪费系统资源。
- 如果确保这个实例在运行中肯定要被访问,那选择这种方式也是没有问题的。
懒汉式(线程不安全)
private static Singleton_02 singleton_02;
private Singleton_02() {}
public static Singleton_02 getBean() {
if (null == singleton_02) {
return new Singleton_02();
}
return singleton_02;
}
- 延迟加载的实现方式,调用getBean方法才会去实例化类
- 线程不安全,如果多个线程同时进入getBean方法,第一个线程还未实例化new Singleton_02()完成,第二个线程就通过了if (null == singleton_02)判断,就可能会造成类多次实例化。
懒汉式(线程安全)
那么,为了解决上述这种方式带来线程不安全的问题,我们可以通过加锁的思路来尝试完善。
private static Singleton_03 singleton_03;
private Singleton_03() {}
public static synchronized Singleton_03 getBean() {
if (null != singleton_03) {
return singleton_03;
}
singleton_03 = new Singleton_03();
return singleton_03;
}
- 使用synchronized 关键字对方法进行加锁,让方法同一时间只能有一个线程进入,也能够避免多次实例化的问题
- 但是,把锁加到方法上,所有的访问都需要等待锁,也导致了资源的浪费
双重校验锁(推荐)
那怎么样才能避免资源浪费,又能保证线程安全,避免多次实例化呢?
private static Singleton_05 singleton_05;
private Singleton_05() {}
public static Singleton_05 getBean() {
if (null != singleton_05) {
return singleton_05;
}
synchronized (Singleton_05.class) {
if (null == singleton_05) {
singleton_05 = new Singleton_05();
}
return singleton_05;
}
}
- 双重校验锁的方式是对上述方式的优化,减少了获取实例的耗时
- 双重校验锁支持懒加载,并且保证了线程安全,还有效提升了性能,简直完美,推荐使用。
静态内部类(推荐)
还有静态内部类也是非常推荐的一种单例实现方式,重点哦
private Singleton_06(){}
private static class innerClass {
private static Singleton_06 instance = new Singleton_06();
}
public static Singleton_06 getInstance() {
return innerClass.instance;
}
- 使用静态内部类实现的单例模式,既保证了线程安全又保证了懒加载,同时也不会因为加锁而耗费性能
枚举实现(推荐)
public enum Singleton_07 {
INSTENCE
;
void getBean() {
System.out.println("枚举方式实现单例模式");
}
}
// 调用方式
public void test() {
Singleton_07.INSTENCE.getBean();
}
- Effective Java 作者推荐使⽤枚举的⽅式解决单例模式,此种⽅式平时比较少⽤到,但它是公认的实现单例的最佳方法
补充:CAS(AtomicReference)
Java并发库提供了很多原子类来支持并发访问的数据安全性,AtomicInteger、AtomicBoolean、AtomicLong、AtomicReference
AtomicReference可以封装引用一个实例,支持并发访问
private static final AtomicReference<Singleton_04> INSTANCE = new AtomicReference<Singleton_04>();
private static Singleton_04 instance;
private Singleton_04(){}
public static final Singleton_04 getInstance() {
while (true) {
Singleton_04 singleton_04 = INSTANCE.get();
if (null != instance) {
return instance;
}
INSTANCE.compareAndSet(null, new Singleton_04());
return INSTANCE.get();
}
}
- 使用CAS的好处就是不需要使用传统的加锁方式保证线程安全,而是依赖于CAS的忙等算法,依赖于底层的硬件实现,来保证线程安全。
- 相对于其他锁的实现没有线程的切换和阻塞也就没有了额外的开销,并且可以支持较大的并发性
- 缺点就是忙等,如果一直没有获取到将会处于死循环中
适用场景
工具类对象、以及频繁访问数据库或文件的对象(比如数据源,session工厂)
总结
总的来说,在平时开发中,一般比较推荐的单例实现方式就是饿汉式、双重校验锁、静态内部类、枚举实现、CAS(AtomicReference),具体使用哪种方式还要根据业务场景具体情况具体分析,前提是需要充分了解各种实现方式的优缺点。