设计模式——单例模式

单例模式

  • 单例模式是我们在编程过程中经常使用的一种设计模式,它属于一种创建型模式。在这种设计模式中涉及到一个需要保证单例的一个类,通过这个类自身去创建本类的一个对象,同时在代码中的任何地方,能够获取到的此类对象都是同一个对象,从而保证此类的对象有且仅被创建一次。
  • 此类提供一个静态的公共方法(public static T getInstance),通过公共方法,可以在代码中全局地使用这个类的单例对象。

应用场景

  • 在代码中,有些工具类等类型的类,并不需要每次使用的时候都去创建,只需要有一个全局的变量,这样做可以减少内存中对象存在的数量,也可以减少虚拟机频繁的触发GC。

实际案例

  • 对属性资源文件的读取,某个Property资源文件,在第一次创建并读取本地资源文件后,通过单例保存只被创建一次,可以全局的使用。
  • 在Web中,我们需要统计同时在线的用户数量,可以通过全局的一个对象来保存访问人数,这个全局的对象,应该是单例形式的,在第一次web启动或第一次有人访问的时候,实例化该单例对象。
  • Spring中通过Singleton一级缓存的方式来储存单例对象,从而在使用的时候,可以很方便地将我们的单例对象注入到某一个其它 Bean中。
  • Spring中的ApplicationContext对象,Spring中通过ApplicationContext对象来存储上下文的一些信息,Spring应用程序在启动的时候就会去进行创建,而程序中自定义的bean都会被添加至ApplicationContext中的BeanFactory里去。

单例模式常见写法及优缺点分析

一、饿汉式

public class Singleton1 {
    private static final Singleton1 instance = new Singleton1();
    /*去掉final  效果与上面一样的
    static {
        instance = new Singleton1();
    }*/
    private Singleton1() {

    }
    public static Singleton1 getInstance() {
        return instance;
    }
}

饿汉式,在类加载的时候,对类里的static对象,或static静态代码块进行初始化操作。同时,由于类加载通过是由JVM去完成,所以该操作是线程安全的。
这种写法,也是我们平时编程中用得最多的写法,它虽然即能够保证单例,也能够保证线程安全,但由于该单例写法是在类加载的时候就对单例对象进行初始化,所以有可能会导致加载速度较慢。

二、懒汉式

public class Singleton2 {
    private static  Singleton2 instance;


    private Singleton2() {

    }

    public static Singleton2 getInstance() {
        if (instance == null) {
            instance = new Singleton2();
        }
        return instance;
    }
}

懒汉式的写法,在多个线程同时调用getInstance方法的时候,会存在线程不安全的情况发生,导致多个线程获取到的“单例对象”不一致。所以这种写法无法保证对象的单例性。

三、懒汉式的基础上通过加锁的方式保证对象单例

public class Singleton3 {

    private static Singleton3 instance;

    private Singleton3() {

    }


    /**
     * 方法上加锁
     * @return
     */
    public synchronized static Singleton3 getInstance() {
        if (instance == null) {
            instance = new Singleton3();
        }
        return instance;
    }
}

这种写法通过在getInstance的方法加sync同步锁,来保证获取到的对象是同一个单例对象,虽然能够保证线程安全,但如果在大量线程同时调用的时候,可以明显感觉到速度有所下降。

四、懒汉式的基础上通过在方法体内部加锁的方式来保证单例对象

public class Singleton4 {
    private static Singleton4 instance;

    private Singleton4() {


    }

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

如果仔细分析,这种方法其实并不能保证单例,因为如果多个线程运行到if判断的时候,虽然创建过程加了锁,但在判断的时候,还是会有多个线程判断为Null。

五、DCL双重检查机制,确保线程安全

public class Singleton5 {

    private static Singleton5 instance;

    private Singleton5() {


    }

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

通过双重校验的机制,能够保证代码执行在多线程环境下的线程安全。但是这里会有一个地方会产生错误,Sync虽然通过锁的方式保证了单例对象的唯一性,但由于代码在执行过程中经过了编译优化,对象的创建步骤有可能会被指令重排序。简单来讲,对象的创建过程可以分为以下几个步骤

在以上三个过程中,如果 2和3 发生了指令重排序,在这种情况下
变成了这样

两个线程调用 getInstance,如果线程1拿到了锁,进入了Sync内部,进行new操作,刚好执行完C操作,正要执行B操作,但在此时如果还没有执行完,线程2就得到了CPU执行时间片,线程2判断if语句就不为Null,并将其返回,所以线程2有可能拿到初始化一半的对象,使用初始化一半的对象可能会导致奇怪的现象发生,甚至导致整个程序崩溃。

六、DCL+volatile关键字,确保线程安全,解决上述问题

public class Singleton6 {
    private volatile static Singleton6 instance;

    private Singleton6() {
    }

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

通过对单例对象添加volatile 关键字,可以禁用对象创建过程中的指令重排序,从而解决上述问题

七、通过静态内部类的方式,由JVM类加载的时候对对象进行初始化

public class Singleton7 {
    private static Singleton7 instance;

    private Singleton7() {

    }

    private static class InnerClass {
        private final static Singleton7 holder = new Singleton7();
    }

    public static Singleton7 getInstance() {
        return InnerClass.holder;
    }
}

这种写法是通过JVM进行类加载的时候初始化单例对象,由于加载外部类的时候并不会加载内部类,所以也实现了懒加载。

八、通过枚举的方式确保单例

public enum Singleton8 {

    INSTANCE;

    public void m() {


    }

    public static void main(String[] args) {
        Singleton8.INSTANCE.m();
    }

}

通过枚举的方式实现单例,不仅解决线程安全,效率高,而且还可以防止单例对象被反序列化

posted @ 2021-03-26 14:37  心若向阳花自开  阅读(113)  评论(1编辑  收藏  举报