[设计模式]单例设计模式的6种实现方式(超全面哟)

无论什么开发中,设计模式都起着关键的作用,其中比较常用的当属单例了~那么什么是单例设计模式呢?

1. 什么是单例设计模式(SINGLETON)

单例模式指的是在应用整个生命周期内只能存在一个实例。单例模式是一种被广泛使用的设计模式。他有很多好处,能够避免实例对象的重复创建,减少创建实例的系统开销,节省内存。

有一个比喻:俺有6个漂亮的老婆,她们的老公都是我,我就是我们家里的老公Sigleton,她们只要说道“老公”,都是指的同一个人,那就是我(刚才做了个梦啦,哪有这么好的事)。

在java语言中,单例带来了两大好处:

1)对于频繁使用的对象,可以省略创建对象所花费的时间,这对于那些重量级的对象而言,是非常可观的一笔系统开销。

2)由于new操作的次数减少,因而对系统内存的使用频率也会降低,这将减轻GC压力,缩短GC停顿时间。

所以对于系统的关键组件和被频繁操作的对象,使用单例模式便可以有效地改善系统性能。

那我们使用静态类创建唯一实例不可以吗?那这里就需要说一下 单例 和 静态类 的区别了。

2. 单例模式和静态类的区别

首先理解一下什么是静态类,静态类就是一个类里面都是静态方法和静态属性,构造器被private修饰,因此不能被实例化。

知道了什么是静态类后,来说一下他们两者之间的区别:

1)首先单例模式会提供给你一个全局唯一的对象,静态类只是提供给你很多静态方法,这些方法不用创建对象,通过类就可以直接调用;

2)单例模式的灵活性更高,方法可以被override,因为静态类都是静态方法,所以不能被override;

3)如果是一个非常重的对象,单例模式可以懒加载,静态类就无法做到;

那么时候时候应该用静态类,什么时候应该用单例模式呢?首先如果你只是想使用一些工具方法,那么最好用静态类,静态类比单例类更快,因为静态的绑定是在编译期进行的。如果你要维护状态信息,或者访问资源时,应该选用单例模式。还可以这样说,当你需要面向对象的能力时(比如继承、多态)时,选用单例类,当你仅仅是提供一些方法时选用静态类。

那么我们该如何去实现单例设计模式呢?

3.单例设计模式的实现方式

(1)饿汉式(我很饿,直接把吃的交上来,我一直在)

public class Singleton {   
    private Singleton() {}                       //1.私有构造函数,不让别的类创建本类对象
    private static Singleton = new Singleton();  //2.创建本类对象
    public static getSignleton(){                //3.对外提供公共的访问方法,返回本类对象
        return singleton;
    }
}

/*此处还可以使用final来实现
    public class Singleton {   
        private Singleton() {}                             
        private final static Singleton = new Singleton();  
   
    }
*/

饿汉式是在第一次引用该类的时候就创建对象实例,而不管实际是否需要创建,假如单例类里面有getSignleton()以外的其它静态方法,跟单例没啥关系的方法,如果使用了Singleton.Xxx()这种调用,就会自动创建Singleton这个类实例(虽然private构造已经防止了你人为去new这个单例),这是开发人员不愿看到的。

好处是编写简单,但是占用资源,无法做到延迟创建对象,因此这种方式适合占用资源少,在初始化的时候就会被用到的类。

我们很多时候都希望对象可以尽可能地延迟加载,从而减小负载,所以就需要下面的懒汉法:

(2)懒汉式(我很懒,啥时候需要我,我再出来)

public class Singleton {
    private Singleton(){}                                 //1.私有构造方法
    private static Singleton singleton = null;            //2.创建本类对象的属性并赋值为null            
    public static Singleton getSingleton() {              //3.对外提供公共的访问方法
        if(singleton == null)                             //在类中判断本类对象是否为空
            singleton = new Singleton();                  //如果为空则创建对象并赋值
        return singleton;                                 //返回本类对象
    }
}

懒汉模式就是单例的延迟加载,也叫懒加载。在程序需要用到的时候再创建实例,这样保证了内存不会被浪费。

这次改良主要是从JVM加载类的原理上进行改良的,上面的代码在初始化类的时候给singleton赋予null,确保系统启动的时候没有额外的负载,其次,在getSingleton()方法中加入判断单例是否存在,不存在才new。

这样写如果是单线程下运行是没有问题的,但如果是在多线程中就可能会出现两个或多个对象,试想一下如果恰好有两个线程同时进入了getSingleton()得if语句里面,这时候就会实例化两次Singleton,因为首次执行getSingleton()时singleton是null,所以这种情况是可能发生的。

所以针对以上两种实现方式,区别就出来了

1)如果都是单线程:

饿汉式是以空间换时间

懒汉式是以时间换空间

2)如果都是多线程:

饿汉式是没有安全隐患的

懒汉式可能会创建多个对象

那我们如何保证懒汉式的线程安全呢?

(3)线程安全下的懒汉式:

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

为了在多线程中不让两个线程同时执行getSingleton()方法,可以为此方法添加一同步锁将对singleton的null判断以及new的部分使用synchronized进行加锁(同步方法或同步代码块都可以),同时,对singleton对象使用volatile关键字进行限制,保证其对所有线程的可见性(这里volitile关键字的作用是,当多线程实例化singleton之后会立即被其他线程看到),并且禁止对其进行指令重排序优化(禁止指令重排优化这条语义直到jdk1.5以后才能正确工作。此前的JDK中即使将变量声明为volatile也无法完全避免重排序所导致的问题。所以,在jdk1.5版本前,双重检查锁形式的单例模式是无法保证线程安全的。)。

这样就可以保证在多线程中也只会创建一个对象,但同步锁是比较耗费资源的,效率太低,是同步运行的,下个线程想要取得对象,就必须要等上一个线程释放,才可以继续执行。所以说,在单例中添加同步锁的方法比较适用于对对象获取不是很频繁地情况。

啊啊啊,又想保证安全,又想效率高点,我该肿么做?

(4)DCL双重检查锁机制:

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

这种写法被称为“双重检查锁”,顾名思义,就是在getSingleton()方法中,进行两次null检查。看似多此一举,但实际上却极大提升了并发度,进而提升了性能。为什么可以提高并发度呢?就像上文说的,在单例中new的情况非常少,绝大多数都是可以并行的读操作。因此在加锁前多进行一次null检查就可以减少绝大多数的加锁操作,执行效率提高的目的也就达到了。

双重检查锁?好高大上呀!听说还有更优雅的实现方式?

(5)枚举写法

public enum Singleton {
    INSTANCE;
    private String name;
    public String getName(){
        return name;
    }
    public void setName(String name){
        this.name = name;
    }
}

使用枚举除了线程安全和防止反射强行调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。因此,Effective Java推荐尽可能地使用枚举来实现单例,但是在Android平台上却是不被推荐的。在这篇Android Training中明确指出:Enums often require more than twice as much memory as static constants. You should strictly avoid using enums on Android.。

有木有更牛X的实现方式呢?有滴有滴!

(6)静态内部类的单例模式(也属于饿汉式) 推荐!

 

public class Singleton {
    private Singleton(){}
    private static class Holder {
        private static Singleton singleton = new Singleton();
    }
    public static Singleton getSingleton(){
        return Holder.singleton;
    }
}

用内部类来保护单例,当Singleton类被加载时,内部类不会被初始化,所以可以确保Singleton类被载入JVM时,不会初始化单例类,当getSingleton()方法被调用时,才会加载Holder,从而初始化singleton,同时,由于实例的建立是在类加载时完成的,故天生对多线程友好,getSingleton()方法也不需要使用synchronized修饰,因此,这种实现能兼顾延迟加载,非同步两种优点。

注:在极端情况下,序列化和反序列化可能会破坏单例,一般来说不多见,如果存在就要多加注意,此时可以加入以下代码:

private Object readResolve() {
     return Holder.singleton;
}

那么readResolve()方法到底是何方神圣,其实当JVM从内存中反序列化地”组装”一个新对象时,就会自动调用这个 readResolve()方法来返回我们指定好的对象了, 单例规则也就得到了保证。readResolve()的出现允许程序员自行控制通过反序列化得到的对象。

最后感谢各位大大写的资料,参考如下:

浅谈单例设计模式的几种实现方式

设计模式之单例模式

Java实现单例的5种方式

你真的会写单例模式吗——Java实现

Java四种单例设计模式

设计模式--单例模式

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章