一、引言
在软件开发中,设计模式是为了解决常见问题的最佳实践。单例模式就是其中之一,它确保一个类只有一个实例,并提供一个全局访问点。这种模式在很多场景中都非常有用,如配置管理、缓存等。
二、单例模式定义及应用场景
单例模式确保某个类只有一个实例,并提供一个全局访问点来获取这个实例。这种模式的主要目的是控制某个类只生成一个对象实例,以避免频繁地创建和销毁对象。
三、单例模式的类型
饿汉式单例:在类加载时就完成了初始化。
懒汉式单例:在第一次调用时才进行初始化。
双重检查锁定单例:结合懒汉式和synchronized实现线程安全。
静态内部类单例:利用了类加载的特性。
枚举单例:利用枚举的天然特性实现线程安全单例。
3.1 饿汉式单例模式
饿汉式单例模式的特点是在类加载时就会初始化实例。这种方式在多线程环境下也是安全的,因为实例在类加载时就已经创建好了。
饿汉式单例模式的实现涉及到以下几个关键点:
私有静态变量:声明一个私有的静态变量来存储单例实例。
构造函数私有化:将构造函数的访问修饰符设置为private,以防止外部代码创建实例。
类加载时初始化:在类加载时就已经创建实例。
代码示例
public class HungryStaticSingleton {
//先静态后动态
//先上,后下
//先属性后方法
private static final HungryStaticSingleton hungrySingleton;
//静态调用
static {
hungrySingleton = new HungryStaticSingleton();
}
private HungryStaticSingleton(){}
public static HungryStaticSingleton getInstance(){
return hungrySingleton;
}
}
饿汉式单例模式定死了初始化时就做创建对象,适用于系统中单例对象较少的场景,由于定死了一个对象,所以在多线程的环境下是安全的。但是在单例对象较多的情况下会占用较大的内存空间。
优点:
线程安全:由于实例在类加载时就已经创建好了,因此在多线程环境下也是安全的。
简单易用:实现方式相对简单,易于理解和维护。
缺点:
提前加载:由于实例在类加载时就已经创建好了,可能会造成不必要的内存占用。如果单例实例需要大量的资源进行初始化,那么这种方式可能会导致性能问题。为了解决这个问题,可以考虑使用延迟加载的方式来实现单例模式。
3.2 懒汉式单例模式
因为懒汉式单例模式在类一创建时就会创建对象,当系统中的单例对象过多时,就会占用大量的内存空间。所以这边采用懒汉式单例模式来解决,懒汉式单例模式和饿汉式单例模式一样,构造方法私有化,在类中提供静态公有构造方法提供唯一实例。与饿汉式单例模式不同的是,懒汉式单例模式的对象要在使用的时候才创建。
懒汉式单例模式的实现涉及到以下几个关键点:
私有静态变量:声明一个私有的静态变量来存储单例实例。
构造函数私有化:将构造函数的访问修饰符设置为private,以防止外部代码创建实例。
延迟加载:在第一次调用某个方法时才进行实例的创建。
** 代码示例**
//懒汉式单例
public class LazySimpleSingletion {
private static LazySimpleSingletion instance;
private LazySimpleSingletion(){}
public static LazySimpleSingletion getInstance(){
if(instance == null){
instance = new LazySimpleSingletion();
}
return instance;
}
}
这种情况解决了系统运行就初始化对象的问题,但是存在线程安全问题。我们来开启两个线程来同时访问该实例。
public class LazySimpleSingletionExcutor implements Runnable{
@Override
public void run() {
LazySimpleSingletion instance = LazySimpleSingletion.getInstance();
System.out.println(Thread.currentThread().getName()+": "+instance);
}
}
public class TestLazySimpleSingletion {
public static void main(String[] args) {
Thread thread1 = new Thread(new LazySimpleSingletionExcutor());
Thread thread2 = new Thread(new LazySimpleSingletionExcutor());
thread1.start();
thread2.start();
System.out.println("end");
}
}
这里只有两个线程,在线程量过多的情况下,是有可能出现多个不同的实例的。为了解决这种问题,我们可以在方法上加上synchronized关键字。
public class LazySimpleSingletion {
private static LazySimpleSingletion instance;
private LazySimpleSingletion(){}
public static synchronized LazySimpleSingletion getInstance(){
if(instance == null){
instance = new LazySimpleSingletion();
}
return instance;
}
}
优点:
延迟加载:只有在第一次使用时才创建实例,节省了内存空间。
实现简单:相对于其他单例模式实现方式,懒汉式单例模式的实现较为简单。
缺点:
线程安全问题:如果没有正确处理线程安全问题,可能会导致多个实例的产生。上述示例代码通过同步方法解决了线程安全问题,但存在性能开销。
反射攻击:通过反射仍然可以破坏单例模式。为了防止反射攻击,可以在构造函数上添加访问权限控制或使用其他方式保护单例模式。
3.3 双重检查锁定单例
双重检查锁定单例模式结合了懒汉式和synchronized的特性,以实现线程安全的单例实例创建。这种模式的核心思想是在第一次使用时才进行实例的创建,并利用volatile和synchronized关键字确保线程安全。
双重检查锁定单例模式的实现涉及到以下几个关键点:
私有静态变量:声明一个私有的静态变量来存储单例实例。
构造函数私有化:将构造函数的访问修饰符设置为private,以防止外部代码创建实例。
双重检查锁定:在getInstance()方法中进行双重检查锁定,以确保线程安全。
volatile关键字:使用volatile关键字确保多线程环境下的可见性。
代码示例
public class Singleton {
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized (Singleton.class) { // 同步块
if (instance == null) { // 第二次检查
instance = new Singleton();
}
}
}
return instance;
}
}
在上述代码中,首先检查instance是否为null,如果是null则进入同步块,再次检查instance是否为null。只有在两次检查都为null的情况下才会创建Singleton实例。这样可以确保即使在多线程环境下,也只有一个线程能够创建实例。同时,使用volatile关键字可以确保多线程环境下的可见性,保证所有线程都能正确地获取到单例实例。
优点:
延迟加载:只有在第一次使用时才创建实例,节省了内存空间。
线程安全:通过双重检查锁定和volatile关键字实现了线程安全。
简单易用:实现方式相对简单,易于理解和维护。
缺点:
性能开销:由于涉及到同步操作,存在一定的性能开销。
无法防止反射攻击:通过反射仍然可以破坏单例模式。为了防止反射攻击,可以在getInstance()方法上添加额外的校验逻辑。
序列化问题:如果Singleton类实现了Serializable接口,那么在序列化过程中可能会创建多个实例。为了解决这个问题,可以将Singleton类声明为final并重写readResolve()方法来确保序列化后仍然是同一个实例。
3.4 静态内部类单例
静态内部类单例模式的特点是使用静态内部类来实现单例。这种方式利用了类加载的特性,可以在类加载时自动创建实例,从而避免了线程安全问题。
静态内部类单例模式的实现涉及到以下几个关键点:
私有静态内部类:声明一个私有的静态内部类来存储单例实例。
延迟初始化:在静态内部类中延迟初始化实例。
访问外部类的静态字段:在静态内部类中访问外部类的静态字段。
代码示例
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton() {}
public static Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
在上述代码中,我们使用了一个私有的静态内部类SingletonHolder来存储单例实例。由于该内部类是私有的,因此外部代码无法直接访问。当getInstance()方法被调用时,会返回SingletonHolder.INSTANCE,即Singleton的实例。这种方式利用了类加载的特性,可以在类加载时自动创建实例,从而避免了线程安全问题。需要注意的是,由于构造函数是私有的,因此外部代码无法通过new Singleton()来创建实例。此外,该方式还可以避免反序列化时重新创建新的实例。
优点:
线程安全:由于实例在类加载时就已经创建好了,因此在多线程环境下也是安全的。
延迟加载:只有在第一次使用时才创建实例,节省了内存空间。
避免反序列化重新创建新的实例:该方式可以确保反序列化时仍然是同一个实例。
缺点:
实现较为复杂:相对于其他单例模式实现方式,静态内部类单例模式的实现较为复杂。
3.5枚举单例
枚举单例模式的特点是使用枚举类型来实现单例。由于枚举类型在Java中的特性,这种方式被认为是实现单例的最佳实践。
枚举单例模式的实现涉及到以下几个关键点:
定义枚举类型:声明一个枚举类型来存储单例实例。
构造函数私有化:将构造函数的访问修饰符设置为private,以防止外部代码创建实例。
提供一个公开的静态常量:在枚举类型中提供一个公开的静态常量来获取单例实例。
代码示例
public enum Singleton {
INSTANCE;
public void someMethod() {
// ...
}
}
在上述代码中,我们定义了一个名为Singleton的枚举类型,并在其中存储了一个公开的静态常量INSTANCE。由于枚举类型的特性,INSTANCE在枚举类型被加载时就已经创建好了,因此这种方式是线程安全的。我们可以通过Singleton.INSTANCE来获取单例实例,并调用其中的方法。由于构造函数是私有的,因此外部代码无法通过new Singleton()来创建实例。此外,由于枚举类型的特性,反序列化时也不会重新创建新的实例。
优点:
线程安全:由于实例在类加载时就已经创建好了,因此在多线程环境下也是安全的。
简单易用:实现方式相对简单,易于理解和维护。
避免反序列化重新创建新的实例:该方式可以确保反序列化时仍然是同一个实例。
天然的只读属性:由于枚举类型的特性,单例实例是天然的只读属性。
缺点:
适用场景有限:由于枚举类型的使用场景有限,因此这种方式适用于那些适合使用枚举类型的场景。
四、总结
单例模式是Java中最常用的设计模式之一。通过使用不同的实现方式,我们可以根据实际需求选择最适合的模式。理解单例模式的各种实现方式以及其适用场景,对于提高代码质量和效率至关重要。