本文最后更新于 2025-03-23,文章超过7天没更新,应该是已完结了~

什么是单例模式?

单例模式(Singleton Pattern)是最简单的创建型设计模式。它会确保一个类只有一个实例存在。

单例模式最重要的特点就是构造函数私有,从而避免外界直接使用构造函数直接实例化该类的对象。

单例模式在Java种通常有两种表现形式: - 饿汉式:类加载时就进行对象实例化 - 懒汉式:第一次引用类时才进行对象实例化

在一个对象需要频繁的销毁、创建,而销毁、创建性能又无法优化时,单例模式的优势尤其明显 - 在一个对象的产生需要比较多资源时,如读取配置、产生其他依赖对象时,则可以通过在启用时直接产生一个单例对象,然后用永久驻留内存的方式来解决 - 单例模式可以避免对资源的多重占用,因为只有一个实例,避免了对一个共享资源的并发操作 - 单例模式可以在系统设置全局的访问点,优化和共享资源访问

1.饿汉式

public class SingletonTest01 {
    public static void main(String[] args) {
        Singleton instance1=Singleton.getInstance();
        Singleton instance2=Singleton.getInstance();
        System.out.println(instance1==instance2);
    }
}
//饿汉式(静态变量)
class Singleton{
    
    //构造函数定义成private,禁止外部创建Singleton实例
	private Singleton(){}
    
    //1.本类内部创建对象实例
    private static final Singleton instance=new Singleton();
    //2.提供一个公有的静态方法,返回实例对象
    public static Singleton getInstance(){
        return  instance;
    }
}

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

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

2.懒汉式(线程不安全)

public class SingletonTest03 {
    public static void main(String[] args) {
        Singleton instance1= Singleton.getInstance();
        Singleton instance2= Singleton.getInstance();
        System.out.println(instance1==instance2);
    }
}
class Singleton{
    private static Singleton instance = null;
    private Singleton(){}
    //提供一个静态的公有方法,当使用到该方法时,才会创建instance
    //懒汉式写法,线程不安全
    public static Singleton getInstance(){
        if (instance==null){
            instance=new Singleton();
        }
        return instance;
    }
}

起到了懒加载的效果,但是只能在单线程下使用

4.懒汉式(线程安全)

public class SingletonTest04 {
    public static void main(String[] args) {
        Singleton instance1= Singleton.getInstance();
        Singleton instance2= Singleton.getInstance();
        System.out.println(instance1==instance2);
    }
}

class Singleton{
    private static Singleton instance;
	private Singleton(){}
    //懒汉式线程安全写法
    public static synchronized Singleton getInstance(){
        if (instance==null){
            instance=new Singleton();
        }
        return instance;
    }
}

解决了线程安全问题;

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

接下来介绍推荐使用的方法

5.双重检查

public class SingletonTest05 {
    public static void main(String[] args) {
        Singleton instance1= Singleton.getInstance();
        Singleton instance2= Singleton.getInstance();
        System.out.println(instance1==instance2);
    }
}
class Singleton{
    private static Singleton singleton;
	private Singleton(){}
    public static Singleton getInstance(){
        //双重检查  适合多线程+有懒加载机制
     
        if(singleton==null){
            synchronized (Singleton.class){
                if(singleton==null){
                    singleton=new Singleton();
                }
            }
        }
        return singleton;
    }
}

当还没有实例 且 多线程的情况下 都进来第一层if判断-->通过, 然后第一个抢占到锁的线程经过第二层if判断-->通过,然后创建了实例 ;第二个线程经过第二次if判断-->发现有实例--->退出,以后任何线程都直接返回第一次创建的实例

7.枚举(最推荐)

public class SingletonTest07 {
    public static void main(String[] args) {
        Singleton instance = Singleton.INSTANCE;
    }
}
enum Singleton{
    INSTANCE;
    Singleton(){
    }
}

这借助了JDK1.5中添加的枚举来实现单例模式,不仅能避免多线程同步问题,代码简洁,而且还能防止反序列化重新创建新的对象

补充:什么是枚举?

我们学习过单例模式,即一个类只有一个实例。而枚举其实就是多例,一个类有多个实例,但实例的个数不是无穷的,是有限个数的。我们称呼枚举类中实例为枚举项。一般一个枚举类的枚举项的个数不应该太多,如果一个枚举类有30个枚举项就太多了!

2.定义枚举类型

定义枚举类型需要使用enum关键字,例如:

public enum Direction {
    FRONT, BEHIND, LEFT, RIGHT;
}
Direction d = Direction.FRONT;

注意,定义枚举类的关键字是enum,而不是Enum,所有关键字都是小写的!

其中FRONT、BEHIND、LEFT、RIGHT都是枚举项,它们都是本类的实例,本类一共就只有四个实例对象。

在定义枚举项时,多个枚举项之间使用逗号分隔,最后一个枚举项后需要给出分号。不能使用new来创建枚举类的对象,因为枚举类中的实例就是类中的枚举项,所以在类外只能使用类名.枚举项。

3.枚举与switch

枚举类型可以在switch中使用

Direction d = Direction.FRONT;

switch(d) {

case FRONT: System.out.println("前面");break;

case BEHIND:System.out.println("后面");break;

case LEFT: System.out.println("左面");break;

case RIGHT: System.out.println("右面");break;

default:System.out.println("错误的方向");

}

Direction d1 = d;

System.out.println(d1);

注意,在switch中,不能使用枚举类名称,例如:“case Direction.FRONT:”这是错误的,因为编译器会根据switch中d的类型来判定每个枚举类型,在case中必须直接给出与d相同类型的枚举选项,而不能再有类型。

4.所有枚举类都是Enum的子类

所有枚举类都默认是Enum类的子类,无需我们使用extends来继承。这说明Enum中的方法所有枚举类都拥有。

5.枚举类的构造器

枚举类也可以有构造器,构造器默认都是private修饰,而且只能是private。因为枚举类的实例不能让外界来创建!

enum Direction {
    FRONT, BEHIND, LEFT, RIGHT;    
    Direction()//枚举类的构造器不可以添加访问修饰符,枚举类的构造器默认是private的。但你自己不能添加private来修饰构造器
    {
        System.out.println("hello");
    }
}

其实创建枚举项就等同于调用本类的无参构造器,所以FRONT、BEHIND、LEFT、RIGHT四个枚举项等同于调用了四次无参构造器,所以你会看到四个hello输出。

6.其实枚举类和正常的类一样,可以有实例变量,实例方法,静态方法等等

为什么说枚举可以解决线程安全问题?

public enum T {
    SPRING,SUMMER,AUTUMN,WINTER;
}

反编译后:

public final class T extends Enum
{
    //省略部分内容
    public static final T SPRING;
    public static final T SUMMER;
    public static final T AUTUMN;
    public static final T WINTER;
    private static final T ENUM$VALUES[];
    static
    {
        SPRING = new T("SPRING", 0);
        SUMMER = new T("SUMMER", 1);
        AUTUMN = new T("AUTUMN", 2);
        WINTER = new T("WINTER", 3);
        ENUM$VALUES = (new T[] {
            SPRING, SUMMER, AUTUMN, WINTER
        });
    }
}

线程安全原理同<clinit>方法加载机制。

那么枚举类的防止反序列化创建实例是为什么?

普通的Java类的反序列化过程中,会通过反射调用类的默认构造函数来初始化对象。所以,即使单例中构造函数是私有的,也会被反射给破坏掉。由于反序列化后的对象是重新new出来的,所以这就破坏了单例。

但是,枚举的反序列化并不是通过反射实现的。所以,也就不会发生由于反序列化导致的单例破坏问题。