单例模式

作者: 起个名忒难 | 来源:发表于2017-06-04 23:23 被阅读137次

    概述

    单例模式是比较简单的设计模式,使用也是非常的广泛。看一下关于单例模式的定义:

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

    它的定义包含了三个要点:

    • 一个类只有一个实例:外部程序无法通过new关键字来创建实例,构造方法用private修饰
    • 自行实例化:由类本身创建实例对象,为了外界可以访问这个实例,需要定义成静态的私有成员变量。
    • 提供实例: 因为无法提供new来床架对象,只能提供一个公有静态方法,返回创建的对象实例。

    来看一下单例模式的通用代码:

    饿汉式单例

    class Singleton {
        private static final Singleton singletonTest = new Singleton();  //创建实例
        private Singleton() {}   //构造方法私有
    
        public static Singleton getInstance(){
            return singletonTest ;  //返回实例
        }
    }
    
    public class SingletonTest{
        public static void main(String[] args) {
            Singleton s1 = Singleton.getInstance() ;
            Singleton s2 = Singleton.getInstance();
    
            System.out.println(s1 == s2);  //true
        }
    }
    

    上面这种写法是最简单的写法,但是有一个问题,就是在类加载的时候就创建了实例,而不管是否需要创建这个实例,没有做到延迟加载 ,增加了负载。

    懒汉式单例

    为了解决上面的问题,对上面的通用代码进行如下改进,当系统需要该实例的时候,才产生实例对象,并返回,代码如下:

    class LazySingleton{
    
        private static  LazySingleton  singleton = null ;
    
        private LazySingleton(){}
    
        public static LazySingleton getInstance(){
            if(singleton == null ){
                singleton = new LazySingleton() ;
            }
            return  singleton ;
        }
    }
    

    通过上面的代码发现,在类加载的时候并没有进行类的初始化操作,只有在第一次调用getInstance()方法时,才进行了实例化操作,实现了延迟加载的功能。那么问题又来了,啥问题问题,上面的类在单线程环境是没有问题的,但是在多线程环境中是不安全的无法保证实例的唯一性,那么在改进一下,看下面代码:

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

    在getInstance的方法上添加了synchronized关键字,来保证线程之间的同步,确保实例的唯一性。看起来已经解决了上面的问题,但是新的问题其实又出现了,什么问题呢? 就是每次访问getInstance()方法时都进进行线程锁定的判断,在多线程高并发的环境下,使得系统的性能大大的降低了,那么有什么办法来解决这个问题吗? 当然有,看下面代码:

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

    最初为了保证线程之间的同步问题,给getInstance()的整个方法加了锁,其实要保证同步的罪魁祸首其实是 singleton=new LazySingleton()这句,只要保证了它的同步,产生的实例就是唯一的,所以使用synchronized块,来保证线程之间的同步。 这样在完成了singleton初始化之后,便不会在进入synchronized中,系统的性能便不会受影响了。 程序看起来已经很完美了,事实确实是这样吗? 当然不是,再次被打脸了,分析一下,假设现在有两个线程A ,B 都执行到了 if() 出,线程A先执行了,获得了锁,此时完成了singleton的初始化操作,释放锁 ,B开始执行,因为B并不知道此时singleton已经完成了实例的创建,会再次创建一个实例。 好的,我们在改进一下 :

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

    在synchronized块中,又进行了一次条件判断,这种方式称为双重检查锁定。 另外在变量的声明中添加了volatile关键字。关于volatile简单说两句,由于singleton使用了volatile修饰,一旦一个线程对改变做了修改,会马上由工作内存写回到主内存中,被其他线程所读取,工作内存可以理解为被线程独享,主内存可以理解为线程共享,被volatile修饰的成员变量可以确保多个线程都能够正确的处理,但是该代码只能在jdk1.5及以上的版本中才能正确执行。现在9都快要发布了, 相信现在还运行在1.5以下的程序应该不是很多了,应该影响不大, 这里只是提一下。 volatile关键字会禁止指令重排序优化,屏蔽到java虚拟机所做的一下代码优化,可能会导致运行效率降低, 这看起来也不是一个非常完美的实现方式。

    饿汉式单例和懒汉式单例的比较

    • 饿汉式单例在类加载时就完成了初始化操作,因此无须考虑多线程的并发访问问题,正是由于它一开始就进行了实例化操作,从资源的利用和加载的时间上考虑可能比懒汉式要差一点
    • 懒汉式单例实现了延迟加载,但是在多线程并发访问的情况下对性能还是有一定的影响。

    既然饿汉式和懒汉式都存在自己的问题,有没有一种方式能解决它们的问题呢,当然有,看下面代码:

    通过使用静态内部类实现单例

    public class StaticInnerClass {
        private StaticInnerClass(){}
        private static class InnerClass{
            private static final StaticInnerClass singleton = new StaticInnerClass() ;
        }
    
        public static StaticInnerClass getInstance(){
            return InnerClass.singleton ;
        }
    }
    

    调用getInstance()方法时,才进行实例的初始化操作,由于是static变量只会进行一次初始化操作,有JVM来保证线程的安全性。 即实现了延迟加载,有可以保证线程的安全。

    枚举实现单例

    //单例类
    class Resource{
        public Resource(){
            System.out.println("resouce 构造方法");
        }
    }
    
    //枚举类生成Resource的单个实例
    public enum EnumSingleton{
        INSTANCE ;
    
        private Resource resource ;
        private EnumSingleton(){
            System.out.println("EnumSingleton 构造方法执行");
            resource = new Resource() ;
        }
    
        public Resource getInstance(){
            return  resource ;
        }
    }
    
    //测试类及运行结果
    public class Test {
        public static void main(String[] args) {
            Resource s1 = EnumSingleton.INSTANCE.getInstance() ;
            Resource s4 = EnumSingleton.INSTANCE.getInstance() ;
            Resource s3 = EnumSingleton.INSTANCE.getInstance() ;
            Resource s2 = EnumSingleton.INSTANCE.getInstance() ;
    
            if(s1==s2 && s2 ==s3 && s3==s4 && s1 ==s4){
                System.out.println("同一个引用");
            }
        }
    }
    
    //运行结果
    EnumSingleton 构造方法执行
    resouce 构造方法
    同一个引用
    

    在枚举类中构造方法私有化,外部无法访问,在访问枚举实例时会执行构造方法,实例化Resouce对象,枚举实例都是static final类型的,只能被实例化一次。这也是effective java中推荐的方式。

    单例模式的优点:

    • 在内存中只有一个实例,减少了内存的开支,无须频繁的创建、销毁
    • 减少了系统的性能开销,避免了资源的多重占用

    缺点:

    • 没有接口,扩展相对比较困难,违背了单一职责的原则

    使用场景:

    要求一个类有且仅有一个对象,只允许有一个公共的访问节点。 可以使用单例模式。


    少年听雨歌楼上,红烛昏罗帐。  
    壮年听雨客舟中,江阔云低,断雁叫西风。
    感谢支持!
                                            ---起个名忒难
    

    相关文章

      网友评论

        本文标题:单例模式

        本文链接:https://www.haomeiwen.com/subject/uabbfxtx.html