单例模式(Singleton)

xiaohai 2021-06-03 10:29:34 1138人围观 标签: 设计模式  Java 
简介单例模式(Singleton Pattern)是最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

模式定义

指一个类只有一个实例,且该类能自行创建这个实例的一种模式。例如,Windows 中只能打开一个任务管理器,这样可以避免因打开多个任务管理器窗口而造成内存资源的浪费,或出现各个窗口显示内容的不一致等错误。

模式特性

  • 单例类只有一个实例对象;
  • 该单例对象必须由单例类自行创建;
  • 单例类对外提供一个访问该单例的全局访问点;

单例模式是设计模式中最简单的模式之一。通常,普通类的构造函数是公有的,外部类可以通过“new 构造函数()”来生成多个实例。但是,如果将类的构造函数设为私有的,外部类就无法调用该构造函数,也就无法生成多个实例。这时该类自身必须定义一个静态私有实例,并向外提供一个静态的公有函数用于创建或获取该静态私有实例。

应用场景

  • 外部资源:每台计算机有若干个打印机,但只能有一个PrinterSpooler,以避免两个打印作业同时输出到打印机。内部资源:大多数软件都有一个(或多个)属性文件存放系统配置,这样的系统应该有一个对象管理这些属性文件
  • Windows的Task Manager(任务管理器)就是很典型的单例模式(这个很熟悉吧),想想看,是不是呢,你能打开两个windows task manager吗? 不信你自己试试看哦~
  • windows的Recycle Bin(回收站)也是典型的单例应用。在整个系统运行过程中,回收站一直维护着仅有的一个实例。
  • 网站的计数器,一般也是采用单例模式实现,否则难以同步。
  • 应用程序的日志应用,一般都何用单例模式实现,这一般是由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加。
  • Web应用的配置对象的读取,一般也应用单例模式,这个是由于配置文件是共享的资源。
  • 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源。数据库软件系统中使用数据库连接池,主要是节省打开或者关闭数据库连接所引起的效率损耗,这种效率上的损耗还是非常昂贵的,因为何用单例模式来维护,就可以大大降低这种损耗。
  • 多线程的线程池的设计一般也是采用单例模式,这是由于线程池要方便对池中的线程进行控制。
  • 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统。
  • HttpApplication 也是单位例的典型应用。熟悉ASP.Net(IIS)的整个请求生命周期的人应该知道HttpApplication也是单例模式,所有的HttpModule都共享一个HttpApplication实例.

模式实现

1、饿汉式单例

该模式的特点是类一旦加载就创建一个单例,保证在调用 getInstance 方法之前单例已经存在了。

public class Singleton1 {

    public static final Singleton1 INSTANCE = new Singleton1();

    //构造方法是private,防止其他调用直接New这个类
    private Singleton1() {
    }

    public static Singleton1 getInstance() {
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        Singleton1 s1 = Singleton1.getInstance();
        Singleton1 s2 = Singleton1.getInstance();

        System.out.println(s1 == s2);
    }
}

执行结果:

true

饿汉式单例在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以是线程安全的,可以直接用于多线程而不会出现问题。

缺点:这个类一加载就实例化。

2、饿汉式单例(静态语句块实现)

跟第一个是一样的

package com.company.singleton;

public class Singleton2 {

    public static final Singleton2 INSTANCE;

    //采用静态语句块实现
    static {
        INSTANCE = new Singleton2();
    }

    //构造方法是private,防止其他调用直接New这个类
    private Singleton2() {
    }

    public static Singleton2 getInstance() {
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        Singleton2 s1 = Singleton2.getInstance();
        Singleton2 s2 = Singleton2.getInstance();

        System.out.println(s1 == s2);
    }
}
3、懒汉式单例(线程不安全)

该模式的特点是类加载时没有生成单例,只有当第一次调用 getlnstance 方法时才去创建这个单例。代码如下:

package com.company.singleton;

public class Singleton3 {

    public static Singleton3 INSTANCE;

    //构造方法是private,防止其他调用直接New这个类
    private Singleton3() {
    }

    public static Singleton3 getInstance() {
        if (INSTANCE == null) {
            //这里睡眠1毫秒
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            INSTANCE = new Singleton3();
        }
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        //多线程访问
        for (int i=0;i<100;i++){
            new Thread(()->{
                System.out.println(Singleton3.getInstance().hashCode());
            }).start();
        }
    }
}

但是这个是线性不安全的,上面我们进行多线程访问,并且在实例化的时候进行sleep,运行可以看到如下结果:

79372097
218859975
1100107490
478974226
224049094
1037172763
135417039
79372097
546405844
1575683492
1575683492
1755865877
.....

这里的hashCode是不一样的,所以出现了线程不安全。

4、懒汉式单例-演变一

使用synchronized来加锁实现

package com.company.singleton;

public class Singleton4 {

    public static Singleton4 INSTANCE;

    //构造方法是private,防止其他调用直接New这个类
    private Singleton4() {
    }

    //对象加锁
    public static synchronized Singleton4 getInstance() {
        if (INSTANCE == null) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Singleton4();
        }
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        for (int i=0;i<100;i++){
            new Thread(()->{
                System.out.println(Singleton4.getInstance().hashCode());
            }).start();
        }
    }
}

运行结果后,是一个实例,但是这样的效率就降低了。那有没有加锁的同时提高效率呢?

5、懒汉式单例-演变二

上一个我们是对方法进行加锁,这里我们能否只对代码块进行加锁:

package com.company.singleton;

public class Singleton5 {

    public static Singleton5 INSTANCE;

    //构造方法是private,防止其他调用直接New这个类
    private Singleton5() {
    }

    public static Singleton5 getInstance() {
        if (INSTANCE == null) {
            //只对代码块进行加锁
            synchronized (Singleton5.class){
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                INSTANCE = new Singleton5();
            }
        }
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        for (int i=0;i<100;i++){
            new Thread(()->{
                System.out.println(Singleton5.getInstance().hashCode());
            }).start();
        }
    }
}

通过运行发现,结果不是同一个对象,所以这里有造成了线程不安全。

6、懒汉式单例-演变三

双重判断加锁

package com.company.singleton;

public class Singleton6 {

    public static volatile Singleton6 INSTANCE; //设置volatile,保证 instance 在所有线程中同步

    //构造方法是private,防止其他调用直接New这个类
    private Singleton6() {
    }

    public static Singleton6 getInstance() {
        if (INSTANCE == null) {
            //只对代码块进行加锁
            synchronized (Singleton6.class) {
                if (INSTANCE == null) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new Singleton6();
                }
            }
        }
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                System.out.println(Singleton6.getInstance().hashCode());
            }).start();
        }
    }
}

运行,发现解决了上一个问题,这个算是比较完美的写法。

7、静态内部类实现
package com.company.singleton;

public class Singleton7 {

    private static class Singleton6Holder {
        private static final Singleton7 INSTANCE = new Singleton7();
    }

    //构造方法是private,防止其他调用直接New这个类
    private Singleton7() {
    }

    public static Singleton7 getInstance() {
        return Singleton6Holder.INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                System.out.println(Singleton7.getInstance().hashCode());
            }).start();
        }
    }
}

运行结果显示所有的实例都是一样的,这样也是比较完美的写法。因为JVM加载类只会加载一次,所以这里保证了线程安全。

8、枚举单例

不仅解决线程同步,还可以防止被反序列化。这个java作者在《Effective Java》中实现的单列,称之为完美中的完美。

package com.company.singleton;

public enum  Singleton8 {

    INSTANCE;

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                System.out.println(Singleton8.INSTANCE.hashCode());
            }).start();
        }
    }
}