单例模式(Singleton Pattern)

在我们的系统中,有一些对象其实我们只需要一个,比如说:线程池、缓存、对话框、注册表、日志对象、充当打印机、显卡等设备驱动程序的对象。事实上,这一类对象只能有一个实例,如果制造出多个实例就可能会导致一些问题的产生,比如:程序的行为异常、资源使用过量、或者不一致性的结果。

如何保证一个类只有一个实例并且这个实例易于被访问呢?定义一个全局变量可以确保对象随时都可以被访问,但不能防止我们实例化多个对象。

更好的解决办法是让类自身负责保存它的唯一实例。这个类可以保证没有其他实例被创建,并且它可以提供一个访问该实例的方法。

定义

单例模式(Singleton Pattern):单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。

J2EE 标准中的 ServletContext 和 ServletContextConfig、Spring框架应用中的 ApplicationContext、数据库中的连接池等也都是单例模式。

单例模式的要点有三个:

  • 单例类只能有一个实例对象;
  • 该单例对象必须由单例类自行创建;
  • 单例类对外提供一个访问该单例的全局访问点。

结构

单例模式的主要角色如下。

  • 单例类:包含一个实例且能自行创建这个实例的类。
  • 访问类:使用单例的类。

以懒汉式为例,UML类图如下:

时序图如下:

实现

Ⅰ饿汉式-静态常量

public class Singleton {
    // 构造器私有化,用户无法通过new方法创建该对象实例
    private Singleton() {
    }
    
    // 在静态初始化器中创建单例实例,这段代码保证了线程安全
    private static Singleton uniqueInstance = new Singleton();

    // 提供一个公有的静态方法,返回实例对象
    public static Singleton getInstance() {
        return uniqueInstance;
    }
}

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

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

这种单例模式可用,但可能造成内存浪费。

Ⅱ 饿汉式-静态代码块

public class Singleton {
    private Singleton() {}

    private static Singleton uniqueInstance;

    // 静态代码块中创建实例
    static {
        uniqueInstance = new Singleton();
    }
    
    public static Singleton getInstance() {
        return uniqueInstance;
    }
}

与上面类似,也是在类装载的时候就完成实例化,只不过将类实例化的过程放在了静态代码块中。

这种单例模式可用,但可能造成内存浪费。

Ⅲ 懒汉式-线程不安全

public class Singleton {

    private static Singleton uniqueInstance;

    private Singleton() {
    }
    // 提供一个静态的公有方法,当使用到该方法时,才去创建uniqueInstance
    public static Singleton getUniqueInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}

优点:起到了**懒加载(Lazy Loading)**的效果,如果没有用到该类,那么就不会实例化 uniqueInstance,从而节约资源。

缺点:线程不安全。如果多个线程能够同时进入 if (uniqueInstance == null) ,并且此时 uniqueInstance 为 null,那么会有多个线程执行 uniqueInstance = new Singleton(); 语句,这将导致实例化多次 uniqueInstance

实际开发中,不要使用。

Ⅳ 懒汉式-线程安全

public class Singleton {

    private static Singleton uniqueInstance;

    private Singleton() {
    }
    // 提供一个静态的公有方法,当使用到该方法时,才去创建uniqueInstance
    public static synchronized Singleton getUniqueInstance() {
        if (uniqueInstance == null) {
            uniqueInstance = new Singleton();
        }
        return uniqueInstance;
    }
}

优点:线程安全。只需要对 getUniqueInstance() 方法加锁,那么在一个时间点只能有一个线程能够进入该方法,从而避免了实例化多次 uniqueInstance。

缺点:效率低。每个线程在想获得类的实例时候,执行 getInstance()方法都要先进行同步,即使 uniqueInstance 已经被实例化了,这会让线程阻塞时间过长。

实际开发中,不推荐使用。

Ⅴ 懒汉式-线程安全-双重校验锁

public class Singleton {

    private volatile static Singleton uniqueInstance;

    private Singleton() {
    }
	//提供一个静态的公有方法,加入双重检查代码,解决线程安全问题, 同时解决懒加载问题
    public static Singleton getUniqueInstance() {
        if (uniqueInstance == null) {// 避免已经实例化后的加锁操作
            synchronized (Singleton.class) {
                if (uniqueInstance == null) {// 避免多个线程同时进行实例化操作
                    uniqueInstance = new Singleton();
                }
            }
        }
        return uniqueInstance;
    }
}

**双重校验锁(Double-Check Locking)**概念是多线程开发中常使用到的。假如在uniqueInstance == null的情况下,两个线程都执行了 if 语句,那么两个线程都会进入 if 语句块内。可以肯定都是,两个线程都会执行 uniqueInstance = new Singleton(); 这条语句,只是先后的问题,也就是说肯定会有两次实例化。

因此必须需要使用两个 if 语句:第一个 if 语句用来避免 uniqueInstance 已经被实例化之后的加锁操作;第二个 if 语句进行了加锁,只允许一个线程进入,保证了线程安全,避免出现uniqueInstance == null时两个线程同时进行实例化操作问题。

uniqueInstance采用 volatile关键字修饰也是很有必要的,因为 JVM了性能优化,可能会指令重排。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。而使用 volatile 修饰uniqueInstance后会引入内存屏障(Memory Barrier),保证了JVM的可见性与有序性。(双重校验锁详解可参考之前写的【并发编程】volatile一文)

总之,双重检验锁保证了线程安全,同时懒汉式保证了延迟加载。

效率较高,推荐使用。

Ⅵ 静态内部类

class Singleton {
    //构造器私有化
    private Singleton() {
    }

	// 静态内部类,该类中有一个静态属性 Singleton
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

	// 提供一个静态的公有方法,直接返回 SingletonInstance.INSTANCE
    public static Singleton getUniqueInstance() {
        return SingletonHolder.INSTANCE;
    }
}

当 Singleton 类被加载时,静态内部类 SingletonHolder 没有被加载进内存。只有当调用 getUniqueInstance() 方法从而触发 SingletonHolder.INSTANCE 时 SingletonHolder 才会被加载,此时初始化 INSTANCE 实例,并且 JVM 能确保 INSTANCE 只被实例化一次。

这种方式不仅具有延迟初始化的好处,利用静态内部类特点实现了延迟加载,而且由 JVM 提供了对线程安全的支持。

效率高,推荐使用。

VII 枚举

enum Singleton {
    INSTANCE; //属性

    public void doSomeTing() {
        System.out.println("通过枚举方法实现单例");
    }
}

使用:

public class EnumSingletonTest {

	public static void main(String[] args) {
		Singleton singleton = Singleton.INSTANCE;
		singleton.doSomeThing();// 通过枚举方法实现单例
	}
}

这种方式是Effective Java 作者Josh Bloch 提倡的方式。虽然这种方法还没有广泛采用,但是单元素的枚举类型已经成为实现Singleton的最佳方法。这种方法在功能上与公有域方法相近,但是它更加简洁,无偿提供了序列化机制,绝对防止多次实例化,即使是在面对复杂序列化或者反射攻击的时候。详细分析可参考Java单例模式的7种写法中,为何用Enum枚举实现被认为是最好的方式一文。

推荐使用。

JDK

总结

优点

  • 提供了对唯一实例的受控访问。
  • 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。
  • 允许可变数目的实例。我们可以基于单例模式进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例。

缺点

  • 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难
  • 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
  • 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;Java、运行环境中提供了自动垃圾回收的技术,如果实例化的对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这会导致对象状态的丢失。

适用场景

需要频繁的进行创建和销毁、或者创建时耗时过多或耗费资源过多但又经常用到的对象;工具类对象;频繁访问数据库或文件的对象(比如数据源、session工厂等)。

参考

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 大白 设计师:CSDN官方博客 返回首页