单例模式(懒汉式单例、饿汉式单例、登记式单例)

单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

注意:

  • 1、单例类只能有一个实例。
  • 2、单例类必须自己创建自己的唯一实例。
  • 3、单例类必须给所有其他对象提供这一实例。

介绍

意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

主要解决:一个全局使用的类频繁地创建与销毁。

何时使用:当您想控制实例数目,节省系统资源的时候。

如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。

关键代码:构造函数是私有的。

应用实例:

  • 1、一个班级只有一个班主任。
  • 2、Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。
  • 3、一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。

优点:

  • 1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。
  • 2、避免对资源的多重占用(比如写文件操作)。

缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

使用场景:

  • 1、要求生产唯一序列号。
  • 2、WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
  • 3、创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。

注意事项:getInstance() 方法中需要使用同步锁 synchronized (Singleton.class) 防止多线程同时进入造成 instance 被多次实例化。

单例模式的写法有好几种,这里主要介绍三种:懒汉式单例、饿汉式单例、登记式单例

一、懒汉式单例

//懒汉式单例类.在第一次调用的时候实例化自己
public class Singleton {
    private Singleton() {}
    private static Singleton single=null;
    //静态工厂方法
    public static Singleton getInstance() {
         if (single == null) {
             single = new Singleton();
         }
        return single;
    }
}

Singleton通过将构造方法限定为private避免了类在外部被实例化,在同一个虚拟机范围内,Singleton的唯一实例只能通过getInstance()方法访问。

(事实上,通过Java反射机制是能够实例化构造方法为private的类的,那基本上会使所有的Java单例实现失效。此问题在此处不做讨论,姑且掩耳盗铃地认为反射机制不存在。)

但是以上懒汉式单例的实现没有考虑线程安全问题,它是线程不安全的,并发环境下很可能出现多个Singleton实例,要实现线程安全,有以下三种方式,都是对getInstance这个方法改造,保证了懒汉式单例的线程安全,如果你第一次接触单例模式,对线程安全不是很了解,可以先跳过下面这三小条,去看饿汉式单例,等看完后面再回头考虑线程安全的问题:

1、在getInstance方法上加同步

public static synchronized Singleton getInstance() {
    if (single == null) {
        single = new Singleton();
    }
    return single;
}

2、双重检查锁定

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

3、静态内部类

public class Singleton {
    private static class LazyHolder {
       private static final Singleton INSTANCE = new Singleton();
    }
    private Singleton (){}
    public static final Singleton getInstance() {
       return LazyHolder.INSTANCE;
    }
}

这种比上面1、2都好一些,既实现了线程安全,又避免了同步带来的性能影响。

二、饿汉式单例

//饿汉式单例类.在类初始化时,已经自行实例化
public class Singleton1 {
    private Singleton1() {}
    private static final Singleton1 single = new Singleton1();
    //静态工厂方法
    public static Singleton1 getInstance() {
        return single;
    }
}

饿汉式在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以天生是线程安全的。

三、登记式单例(可忽略)

//类似Spring里面的方法,将类名注册,下次从里面直接获取。
public class Singleton3 {
    private static Map<String,Singleton3> map = new HashMap<String,Singleton3>();
    static{
        Singleton3 single = new Singleton3();
        map.put(single.getClass().getName(), single);
    }
    //保护的默认构造子
    protected Singleton3(){}
    //静态工厂方法,返还此类惟一的实例
    public static Singleton3 getInstance(String name) {
        if(name == null) {
            name = Singleton3.class.getName();
            System.out.println("name == null"+"--->name="+name);
        }
        if(map.get(name) == null) {
            try {
                map.put(name, (Singleton3) Class.forName(name).newInstance());
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        }
        return map.get(name);
    }
    //一个示意性的商业方法
    public String about() {
        return "Hello, I am RegSingleton.";
    }
    public static void main(String[] args) {
        Singleton3 single3 = Singleton3.getInstance(null);
        System.out.println(single3.about());
    }
}

登记式单例实际上维护了一组单例类的实例,将这些实例存放在一个Map(登记薄)中,对于已经登记过的实例,则从Map直接返回,对于没有登记的,则先登记,然后返回。

这里我对登记式单例标记了可忽略,我的理解来说,首先它用的比较少,另外其实内部实现还是用的饿汉式单例,因为其中的static方法块,它的单例在类被装载的时候就被实例化了。

饿汉式和懒汉式区别

从名字上来说,饿汉和懒汉,

饿汉就是类一旦加载,就把单例初始化完成,保证getInstance的时候,单例是已经存在的了,

而懒汉比较懒,只有当调用getInstance的时候,才回去初始化这个单例。

另外从以下两点再区分以下这两种方式:

1、线程安全:

饿汉式天生就是线程安全的,可以直接用于多线程而不会出现问题,

懒汉式本身是非线程安全的,为了实现线程安全有几种写法,分别是上面的1、2、3,这三种实现在资源加载和性能方面有些区别。

2、资源加载和性能:

饿汉式在类创建的同时就实例化一个静态对象出来,不管之后会不会使用这个单例,都会占据一定的内存,但是相应的,在第一次调用时速度也会更快,因为其资源已经初始化完成,

而懒汉式顾名思义,会延迟加载,在第一次使用该单例的时候才会实例化对象出来,第一次调用时要做初始化,如果要做的工作比较多,性能上会有些延迟,之后就和饿汉式一样了。

至于1、2、3这三种实现又有些区别,

第1种,在方法调用上加了同步,虽然线程安全了,但是每次都要同步,会影响性能,毕竟99%的情况下是不需要同步的,

第2种,在getInstance中做了两次null检查,确保了只有第一次调用单例的时候才会做同步,这样也是线程安全的,同时避免了每次都同步的性能损耗

第3种,利用了classloader的机制来保证初始化instance时只有一个线程,所以也是线程安全的,同时没有性能损耗,所以一般我倾向于使用这一种。

什么是线程安全?

如果你的代码所在的进程中有多个线程在同时运行,而这些线程可能会同时运行这段代码。如果每次运行结果和单线程运行的结果是一样的,而且其他的变量的值也和预期的是一样的,就是线程安全的。

或者说:一个类或者程序所提供的接口对于线程来说是原子操作,或者多个线程之间的切换不会导致该接口的执行结果存在二义性,也就是说我们不用考虑同步的问题,那就是线程安全的。

应用

以下是一个单例类使用的例子,以懒汉式为例,这里为了保证线程安全,使用了双重检查锁定的方式:

public class TestSingleton {
    String name = null;  

        private TestSingleton() {
    }  

    private static volatile TestSingleton instance = null;  

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

    public String getName() {
        return name;
    }  

    public void setName(String name) {
        this.name = name;
    }  

    public void printInfo() {
        System.out.println("the name is " + name);
    }  

}

可以看到里面加了volatile关键字来声明单例对象,既然synchronized已经起到了多线程下原子性、有序性、可见性的作用,为什么还要加volatile呢?

public class TMain {
    public static void main(String[] args){
        TestStream ts1 = TestSingleton.getInstance();
        ts1.setName("jason");
        TestStream ts2 = TestSingleton.getInstance();
        ts2.setName("0539");  

        ts1.printInfo();
        ts2.printInfo();  

        if(ts1 == ts2){
            System.out.println("创建的是同一个实例");
        }else{
            System.out.println("创建的不是同一个实例");
        }
    }
}

运行结果:

the name is 0539
the name is 0539
创建的是同一个实例

结论:由结果可以得知单例模式为一个面向对象的应用程序提供了对象惟一的访问点,不管它实现何种功能,整个应用程序都会同享一个实例对象。

对于单例模式的几种实现方式,知道饿汉式和懒汉式的区别,线程安全,资源加载的时机,还有懒汉式为了实现线程安全的3种方式的细微差别。

优点:

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

缺点:

(1)单例模式一般没有接口,扩展很困难,若要扩展,除了修改代码基本上没有第二种途径可以实现。 
(2)单例对象如果持有Context,那么很容易引发内存泄露,此时需要注意传给单例对象的Context最好是Application Context。

ending...

参考博文:

https://www.runoob.com/design-pattern/singleton-pattern.html

https://www.cnblogs.com/crazy-wang-android/p/9054771.html

原文地址:https://www.cnblogs.com/hxun/p/11840839.html

时间: 2024-10-10 14:19:58

单例模式(懒汉式单例、饿汉式单例、登记式单例)的相关文章

[Js-设计模式]单例模式(饿汉,懒汉,登记式)

·单例模式的特点: 1. 单例类只能有一个实例. 2. 单例类必须自己创建自己的唯一实例. 3. 单例类必须给所有其他对象提供这一实例. ·各种单例模式的特点: ·懒汉式是延时加载,在需要的时候才创建对象,避免内存浪费,但存在线程安全问题. ·饿汉式线程安全,类一加载就实例化对象,所以要提前占用系统资源. ·登记式单例模式克服了饿汉以及懒汉单例的不可继承问题,其子类在登记时才被创建,并且子类的实例化方式只能是饿汉式. ·各种单例模式的类图: ·饿汉式单例模式 ·懒汉式单例模式: ·登记式单例模式

单例饿汉式和饱汉式各自的有缺点(转)

单例模式应用于一个类只有一个实例的情况,并且为其实例提供一个全局的访问点. 特点: 1.一个类只有一个实例 2.自己创建这个实例 3.整个系统只能用这个实例 应用场景 外部资源:每台计算机有若干个打印机,但只能有一个PrinterSpooler,以避免两个打印作业同时输出到打印机. 内部资源:大多数软件都有一个(或多个)属性文件存放系统配置,这样的系统应该有一个对象管理这些属性文件. 实现方式 1.饿汉式:单例实例在类装载时就构建,急切初始化.(预先加载法) /** * 饿汉式(推荐) * */

单例模式的实现(饿汉式和懒汉式)

1.懒汉模式. class Singleton { private: static Singleton* m_instance; Singleton(){} public: static Singleton* getInstance(); }; Singleton* Singleton::getInstance() { if(NULL == m_instance) { Lock();//借用其它类来实现,如boost if(NULL == m_instance) { m_instance = n

Java单例模式《一》饿汉式

package com.study.mode; /** * 单例模式:饿汉式. 线程安全. * @ClassName: SingleBean * @author BlueLake * @date 2012年9月5日 下午8:45:10 */ public class SingleBean { //1.私有化构造方法 private SingleBean(){ } //2.创建当前对象的静态实例. private static SingleBean single= new SingleBean()

抽象类 抽象方法 接口 类部类 匿名类部类 设计模式之单例模式(懒汉模式及饿汉模式)

---恢复内容开始--- 抽象类  关键字  abstract 不能被实例化(创建对象),可通过类名调用静态方法 子类继承抽象类必须重写父类的所有抽象方法,然后用多态调用 接口:关键字 interface   类名 implements 接口名 1.接口中只能有抽象方法,并且不能被实例化,通过多态调用 2.接口与接口之间的关系: 继承关系(可以多继承); 类部类: 在类中定义的类 创建类部类对象    外部类名.内部类名  对象名 = new 外部类名().new内部类名() 匿名类部类: 在写

Java面试题之在多线程情况下,单例模式中懒汉和饿汉会有什么问题呢?

懒汉模式和饿汉模式: public class Demo { //private static Single single = new Single();//饿汉模式 private static Single single2 = null;//饿汉模式 public Demo() { if (single2 == null) {//1 single2 = new Single();//2 } } } 在多线程环境下,代码1和代码2之间,这里会有线程安全问题,线程A和线程B同时到代码1和代码2之

单例模式---懒汉模式与饿汉模式

单例模式:1)一个类只能创建一个实例2)构造函数和静态变量(加载类时即初始化)需为private3)get方法应该为public static,可供全局访问 //懒汉模式 public class lazySingleton { private static lazySingleton singleton=null; //懒,所以就不实例化了,加载类较快,但是第一次访问类可能会有点慢 private lazySingleton(){ //nothing } public static lazyS

C++两种单例(饿汉式,懒汉式)

俄汉式 #include <iostream> using namespace std; class A{ public:     static A& getInstance(void){         return s_instance;     } private:     A(int data=0):m_data(data){}     A(A const& that);     int m_data;     static A s_instance; }; A A::

[转]设计模式--单例模式(一)懒汉式和饿汉式

单例模式是设计模式中比较简单的一种.适合于一个类只有一个实例的情况,比如窗口管理器,打印缓冲池和文件系统, 它们都是原型的例子.典型的情况是,那些对象的类型被遍及一个软件系统的不同对象访问,因此需要一个全局的访问 指针,这便是众所周知的单例模式的应用.当然这只有在你确信你不再需要任何多于一个的实例的情况下. 单例模式的用意在于前一段中所关心的.通过单例模式你可以: 一.确保一个类只有一个实例被建立 二.提供了一个对对象的全局访问指针 三.在不影响单例类的客户端的情况下允许将来有多个实例 经典的单