单例模式--设计模式

定义

一个类在整个系统只能有一个实例,这样做的好处是防止创建多个重复对象消耗内存,而且操作 IO 和数据库等都是比较耗资源的,单例可以减少性能的开销
实现单例模式有多种方式,具体如下:

1、饿汉式(线程安全)
public class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton() {
    }
    public static Singleton getInstance() {
        return instance;
    }
}
//1、私有静态化对象
//2、无参构造器
//3、公共静态化获取对象方法

上面就是一个单例模式的简单写法,可以看到在类加载的时候就初始化实例,并且使用的是私有构造函数,目的是为了其他地方不能通过构造函数来创建该类的实例,只能通过公共的 getInstance() 方法获取该类的实例,虽然避免了多线程并发操作导致的线程安全问题,但是会造成内存的浪费,因为还没有使用这个对象就把它加载到内存中了

2、懒汉式(线程不安全)
public class Singleton {
    private static Singleton instance;
    private Singleton() {
    }
    public static Singleton getInstance() {
        if (Objects.isNull(instance)) {
            instance = new Singleton();
        }
        return instance;
    }
}

小结:只有第一次用到的时候才初始化实例,解决了饿汉式造成内存浪费的问题,但是多线程的时候是不安全的。例如有线程 A 与线程 B 同时执行,这时候就有可能 2 个线程都同时执行到 if (instance == null),这样就创建了 2 个实例了。所以这种方式只适用于单线程
例如:

public class Singleton {
    private static Singleton instance;

    private Singleton() {
        System.out.println(Thread.currentThread().getName() + "执行OK");
    }

    public static Singleton getInstance() {
        if (Objects.isNull(instance)) {
            instance = new Singleton();
        }
        return instance;
    }
	//多线程并发
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            new Thread(() -> Singleton.getInstance()).start();
        }
    }
}

输出结果:

Thread-0执行OK
Thread-5执行OK
Thread-4执行OK
Thread-3执行OK
Thread-1执行OK
Thread-2执行OK
3、懒汉式(线程安全)
//双重检测锁模式的懒汉式单例 DCL懒汉式
public static Singleton getInstance() {
    if (Objects.isNull(instance)) {
        synchronized (Singleton.class) {
            if (Objects.isNull(instance)) {
                instance = new Singleton();
            }
        }
    }
    return instance;
}

小结:在 getInstance 方法上加了同步锁,这样有多个线程的时候会等前面的线程执行完了再执行当前线程,可以解决第二种懒汉式的线程安全问题

4、双重校验锁(线程安全)
public class Singleton {
    private static volatile Singleton instance;
    private Singleton() {
    }
    //双重检测锁模式的懒汉式单例 DCL懒汉式
    public static Singleton getInstance() {
        if (Objects.isNull(instance)) {
            synchronized (Singleton.class) {
                if (Objects.isNull(instance)) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

小结:可以看到加了双重 if (instance == null) 的判断,并且同步锁放到了方法内部,新增的第一个判空可以避免每个线程过来都去执行同步锁,可以解决同步锁造成的效率低的问题,但是instance = new Singleton(); 并不是一个原子操作,这一行代码可以分成 3 个步骤:

  • 1、申请内存空间
  • 2、初始化对象
  • 3、返回内存空间地址

编译有可能优化为:

  • 1、申请内存空间
  • 2、返回内存空间地址
  • 3、初始化对象

所以代码中需要使用 volatile 关键字,因为它可以解决指令重排的问题,但是只能在 JDK 1.5 之后生效。而且使用 volatile 关键字也会影响一些性能问题

什么是原子操作?

简单来说,原子操作(atomic)就是不可分割的操作,在计算机中,就是指不会因为线程调度被打断的操作
比如简单的赋值是一个原子操作:m = 6; // 这是个原子操作
假如m原先的值为0,那么对于这个操作,要么执行成功m变成了6,要么是没执行m还是0,而不会出现诸如m=3这种中间态——即使是在并发的线程中。声明并赋值就不是一个原子操作:int n = 6; // 这不是一个原子操作对于这个语句,至少有两个操作:①声明一个变量n②给n赋值为6——这样就会有一个中间状态:变量n已经被声明了但是还没有被赋值的状态。——这样,在多线程中,由于线程执行顺序的不确定性,如果两个线程都使用n,就可能会导致不稳定的结果出现

什么是指令重排?

简单来说,就是计算机为了提高执行效率,会做的一些优化,在不影响最终结果的情况下,可能会对一些语句的执行顺序进行调整
比如这一段代码:

int a ;   // 语句1 
a = 8 ;   // 语句2
int b = 9 ;     // 语句3
int c = a + b ; // 语句4

正常来说,对于顺序结构执行的顺序是自上到下,也即1234。但是,由于指令重排的原因,因为不影响最终的结果,所以,实际执行的顺序可能会变成3124或者1324

5、静态内部类(线程安全)
public class Singleton {
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    private Singleton() {
    }
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

小结:这种方式在 Singleton 类加载的时候并没有初始化实例,而是第一次调用 getInstance() 的时候才进行初始化,可以达到延迟加载对象的作用。并且 JVM 能确保 INSTANCE 只被实例化一次。也就是这种方式可以实现双重校验锁一样的效果,而且解决了使用 volatile 关键字只能在 JDK 1.5 之后生效和影响性能的问题。但是这种方式用的人还是比较少的
但以上方式还会存在一个问题,可通过反射破坏单例

//反射破坏单例
public static void main(String[] args) throws Exception {
    Singleton instance1 = Singleton.getInstance();
    Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor(null);
    constructor.setAccessible(true);
    Singleton instance2 = constructor.newInstance();
    System.out.println(instance1);
    System.out.println(instance2);
}

输出结果:

com.xhy.demo01.gof.Singleton@30f39991
com.xhy.demo01.gof.Singleton@452b3a41
6、枚举(线程安全)
public enum Singleton {
    INSTANCE;
    public Singleton getInstance() {
        return INSTANCE;
    }
}

小结:可以说使用枚举的方式实现单例是目前最完美的方式,这种方式可以防止序列化与反射造成的创建多个实例的问题,而前面的 5 种方式默认情况是无法解决这个问题的。可能使用枚举的唯一缺点是可读性不高
优点

  • 由于单例模式在内存中只有一个实例,对于需要频繁实例化然后销毁的对象可以减少内存开销
  • 由于单例只有一个实例,对于创建比较耗时或者耗资源的对象可以减少系统的性能开销。 可以避免对资源的多重占用
  • 可以在系统设置全局的访问点,优化和共享资源访问

缺点

  • 单例模式一般没有接口,所以扩展困难,需要扩展只能修改原有的代码。
  • 单例模式一般职责过多,所以与单一职责原则是有冲突的

使用场景

  1. 只需要一个实例对象
  2. 需要频繁实例化然后销毁的对象
  3. 创建比较耗时的对象,例如访问IO和数据库等资源
选用哪种方式实现单例模式?

以上 6 种方式各有利弊(线程安全问题、性能问题、代码复杂度与可读性问题),所以需要根据自己的项目选择合适的方式。一般建议使用第三种线程安全的懒汉式(例如系统源码 LocalBroadcastManager 就是使用该种方式),如果涉及到序列化与反射则可以使用第六种枚举的方式

什么情况下使用?

有些对象只有一个,比如配置文件,工具类,线程池,缓存,日志对象等
懒汉式:指全局的实例在第一次使用时构建
饿汉式:指全局的实例在类装载时构建
注:日常我们使用较多的应该是懒汉式的单例,毕竟按需加载才能做到资源的最大化利用
例如单例线程池:

public enum AssetClearEnum {
    INSTANCE;
    private final ExecutorService threadPool;
    AssetClearEnum() {
        threadPool = Executors.newCachedThreadPool();
    }
    public ExecutorService getThreadPool() {
        return threadPool;
    }
}
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Rsun04551

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值