设计模式学习(一):单例模式

jkouu 66 0

单例和方法类

单例应该是最简单而且接触最多的设计模式了。一般来说,数据仓库、本地用户信息管理等用途都可以用单例模式实现。在介绍单例之前,我想首先讨论一下第一次接触单例时自己想的问题:单例和方法类有什么区别吗?

首先先明确概念,这里的方法类,指只含有静态方法和静态成员变量的工具类。可能在刚接触面向对象编程时大家会写很多这样的类。从我们的角度来看,好像方法类和单例没什么不一样,因为它们的方法和成员变量都是唯一的呀。

也许在应对小规模的问题上,单例和方法类没有什么区别。但是一旦情景复杂起来,单例的灵活性就体现了出来:说到底,单例模式作为一个类的对象,具备了面向对象的封装、继承和多态三个特性;而方法类更偏向于面向过程的实现,在继承和多态上并没有单例模式出色(比如方法类很少被继承或者实现接口)

单例的实现

单例的实现有很多种,常见的有懒汉式、饿汉式、双重检查、静态内部类和枚举这五种实现。但是鉴于枚举的实现比较少见,而且可读性比较差,我在学习的时候也没有把它当作重点。所以,下面只介绍前面的四种实现。

懒汉式

懒汉式,顾名思义就是懒。这个懒指的是懒加载,也就是说,单例本身在一开始并没有被创建,只有在被使用时才会被创建。这种实现的好处在于不会浪费内存,因为它在被使用时才会被初始化。但是它的缺点在于不是线程安全的,因为当多个线程同时请求单例时,很可能会同时调用构造函数。实现的代码见下方:

public class Singleton{
    private static Singleton instance;
    
    public static Singleton getInstance(){
        if(instance == null){
            instance = new Singleton();
        }
        return instance;
    }
    
    private Singleton(){
        
    }
}

当然,我们也可以让它变得线程安全,只要在将获取单例的方法加锁就行了。但是因为加了锁,所以效率就会变低,这样的牺牲是不可避免的。线程安全的写法如下:

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

饿汉式

与懒汉式相比,饿汉式就没有那么多问题,因为它在程序开始时就已经创建了单例对象,不存在多个线程同时调用的问题。但是相对的,因为单例一直驻留在内存,这种实现方式对内存的消耗较大。实现的方法见下面:

public class Singleton{
    private static Singleton instance = new Singleton();
    
    public static Singleton getInstance(){
        return instance;
    }
    
    private Singleton(){
        
    }
}

双重检查

双重检查是对线程安全的懒汉式的一个改良。既然加锁的时间开销大,那我们就不在调用函数时加锁,而在创建单例时加锁。因为大部分调用的情况下,单例还是存在的,所以这种方式比线程安全的懒汉式要高效。

这里再解释一下为什么要加volatile关键字。

首先我们回顾一下Object object = new Object()的执行过程。正常情况下,应该是先在堆内存分配内存(类加载的准备阶段),再调用构造函数(类加载种的初始化阶段),最后将内存赋给object引用。

但是,还有不正常的情况。

我们知道,Java为了提高自己的执行性能,会对字节码指令进行重排序。Java规定,这样的重排序是不会影响单线程下的执行结果的。而上面创建一个object的语句,在单线程下,即使先将内存付给引用再调用构造函数也是没问题的。这就导致在多线程下,内存赋给引用和调用构造函数是有可能被重排的。

现在我们假设被重排了。第一个线程调用getInstance函数,进入了同步块里,执行了instance = new Singleton()。正巧,就在内存赋给引用的时候,第二个线程也调用了getInstance函数。因为这时候内存已经赋给引用了,第二个线程在第一次判断单例为空的时候就认为单例已经被实例化了。而实际上,单例还没有调用构造函数初始化,这样就会发生异常。所以我们给instance加上volatile关键字,让它的初始化变为原子操作。

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

静态内部类

静态内部类是一种非常漂亮的实现方式。首先,静态内部类只有在被调用时才会被初始化,这使得这种实现具备了懒汉式实现的节约内存的优点;其次,JVM的类加载机制保证了静态类的初始化是唯一的,这就又克服了懒汉式线程不安全的缺点;第三,因为没有用到锁,所以这种实现比双重检查效率高;最后,就像下面大家看到的一样,代码又非常的简洁。在我看来,这种实现是单例实现中最出色的。

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

 

 

发表评论 取消回复
您必须 [登录] 才能发表评论!
分享