[Java设计模式]单例模式入门Ⅰ

时间:2021-1-8 作者:admin

设计模式:单例模式


什么是单例模式

  • 单例模式(Singleton Pattern)是一个比较简单的模式,实际应用很广泛,比如 Spring 中的Bean实例就是一个单例对象。

  • 定义:确保某一个类 只有一个实例,而且自行实例化并向整个系统提供这个实例。


单例模式的优缺点

优点

  • 只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例。

  • 单例模式可以避免对资源的多重占用,例如一个写文件动作,由于只有一个实例存在内存中,避免对同一个资源文件的同时写操作。

  • 单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。

缺点

  • 单例模式一般没有接口,很难扩展(根据环境而定)。
  • 单例模式与单一职责原则有冲突。一个类应该只实现一个逻辑,而不关心是否是单例的。

单例模式的实现

  • 单例模式有很多的实现方式,但是各种实现的方式都有其优缺点,下面来看看各种的实现方式。
  • 单例模式的实现满足以下几点:
    • 构造方法私有。
    • 有一个静态方法获取该类的实例。
    • 该类保证只有一个实例。

懒汉式

  • 懒汉式是当用到这个对象的时候才会创建。
  • 懒汉式,在需要单例模式类实例时它才创建出来给你(因为很懒)。
  • 优点:只有用到的时候才会创建这个对象,因此节省资源。
  • 简单的实现如下:
/**
  *Singleton类,单例模式类,在类加载时便会创建一个私有静态变量instance,也就是该类的实
  *例,再通过公共接口getInstance()来发布该实例
  */
public class Singleton {  
    private static Singleton instance;
    //私有化构造方法防止外界new对象
    private Singleton (){
        
    }  
  	//公有化静态函数,对外暴露单例对象的接口
    public static Singleton getInstance() {  
    	if (instance == null) {  
        instance = new Singleton();  
    	}  
    	return instance;  
    }  
}

但是这种方式并不能保证这是唯一的单例,在高并发访问下,多个线程同时访问到这个单例时,还是有可能不能保证这个类就是单例的

为了保证线程安全,我们可以加锁,给这个getInstance()方法加上线程同步锁synchronize具体实现如下:

public class Singleton {  
    private static Singleton instance;  
    private Singleton (){
        
    }  
    public static synchronized Singleton getInstance() {  
    if (instance == null) {  
        instance = new Singleton();  
    }  
    return instance;  
    }  
}

但是这种方式一旦加锁,虽然可以保证其实单例且线程安全的,但是在高并发访问下性能必然是受到影响,多个线程都需要用到该单例时,就无法保证速度,需要同步地等待这个单例使用完回到JVM中的堆区(Heap)才可以继续使用这个单例,效率十分的低。

还有一种是双重检查式,两次判断

Double Check Lock(DCL)方式实现单例模式

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;
    }
}

我们可以看到getInstance中对instance进行了两次判断是否为空,第一次判断主要是为了避免不必要的同步问题,第二次判断则是为了在null的情况下创建实例,因为在某些情况下会出现失效问题即DCL失效问题,可以使用volatile关键字处理这个问题,但是同样的,使用了volatile关键字也会对性能有一定的影响。但是优点在于资源利用率高,第一次执行getInstance时对象才被实例化,但是DCL也因为第一次加载时反应慢,所以在高并发情况下也会有一定的缺陷。


饿汉式

  • 饿汉式和懒汉式恰巧相反,在类加载的时候就创建实例。
  • 单例模式类迫不及待的想要创建实例了(因为饿了)
  • 优点:还没用到就创建,浪费资源。
  • 缺点:在类加载的时候就创建,线程安全。
  • 实现如下:
/**
  *这种方式在类加载时就完成了初始化,所以类加载较慢,但是获取对象的速度快。这种方式
  *基于类加载机制,避免了多线程的同步问题。如果从来没有使用过这个实例,则会造成内存
  *的浪费。
  */
public class Singleton {  
    private static Singleton instance = new Singleton();  
    private Singleton (){
        
    }  
    public static Singleton getInstance() {  
    	return instance;  
    }  
}

匿名内部类/静态内部类

  • 利用静态变量、静态代码块、静态方法都是在类加载的时候只加载一次的原理。
  • 实现如下
public class Singleton {
    private static Singleton instance;
    //静态块在类加载时会被执行,也就创建了Singleton类实例
    static{
        instance = new Singleton();
    }
    private Singleton (){
    
    }  
    public static final Singleton getInstance() {  
    	return SingletonHolder.INSTANCE;  
    }  
}
/**
  *Java静态内部类的特性是,加载的时候不会加载内部静态类,使用的时候才会进行加载。
  *第一次加载Singleton类时并不会初始化sInstance,只有第一次调用getInstance方法时虚
  *拟机加载SingletonHolder并初始化sInstance。这样不仅能确保线程安全,也能保证
  *Singleton类的唯一性。所以,推荐使用静态内部类单例模式
  */
public class Singleton {  
    private static class SingletonHolder {  
    	private static final Singleton INSTANCE = new Singleton();  
    }
    private Singleton (){
    
    }  
    public static final Singleton getInstance() {  
    	return SingletonHolder.INSTANCE;  
    }  
}

枚举单例模式

/**
  *默认枚举实例的创建是线程安全的,并且在任何情况下都是单例。
  *枚举单例的有点就是简单,缺点是可读性不高。
  */
public enum Singleton {
    //外部调用由原来的Singleton.getInstance变成了Singleton.INSTANCE了。
    INSTANCE;
}

总结

单例模式是运用频率很高的模式,在我们客户端通常是没有高并发的情况,所以选择哪种方式并不会有太大的影响。出于效率考虑,推荐使用静态内部类的单例模式和DCL的单例模式。


优点:

  • 由于单例模式在内存中只有一个实例,减少内存开支,特别是一个对象需要频繁创建、销毁时,而且创建或销毁时性能又无法优化,单例模式的优势就十分明显。
  • 由于单例模式只生成一个实例,所以减少了系统的性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可通过在应用启动时直接产生一个单例对象,然后用永久驻留的方式解决。
  • 单例模式可以避免对资源的多重占用,如一个文件的操作,由于只有一个实例存在内存中,避免对同一个资源文件的同时操作。
  • 单例模式可以在系统设置全局的访问点,优化和共享资源访问。例如,可以设计一个单例类,负责所有数据表的映射处理。

缺点:

  • 单例模式一般没有接口,扩展很困难,除非修改代码。
  • 单例对象如果持有Context,那么很容易引发内存泄露,此时需要注意传递给单例对象的Context最好是Application Context
  • 不适合用于变化频繁的对象;如果实例化的对象长时间不被利用,系统会认为该对象是垃圾而被回收,这可能会导致对象状态的丢失;

使用场景

  • 网站访问量计数器。
  • 项目中用于读取配置文件的类。
  • Spring中,每个Bean默认都是单例的,这样便于Spring容器进行管理。

声明:本文内容由互联网用户自发贡献自行上传,本网站不拥有所有权,未作人工编辑处理,也不承担相关法律责任。如果您发现有涉嫌版权的内容,欢迎进行举报,并提供相关证据,工作人员会在5个工作日内联系你,一经查实,本站将立刻删除涉嫌侵权内容。