设计模式之单例模式

设计模式之单例模式

一.什么是单例模式?

单例模式是设计模式中最简单的一个设计模式,属于创建型模式,其定义是保证一个类***仅有一个实例***,并且提供一个访问它的全局访问点。

单例模式一般体现在类声明中,单例的类负责创建自己的对象,同时保证只有单个对象被创建,并且提供一个访问它的全局访问点,可以直接访问,不需要实例化对象。

二.为什么需要用到单例模式?

  • 由于单例模式只创建了一个实例,减少了内存的消耗,在一些频繁创建销毁实例的情景下可使用
  • 避免对资源的多重占用,比如文件写操作
  • 在一些特定的情境下必须使用单例,如一个国家只能有一个主席

三.如何使用单例?

我们知道一个类的对象的产生是通过它的构造函数来实现的,如果一个类对外提供了一个public的构造方法,那么外界就可以任意创建这个类的对象。所以当你想限制类的产生时,就需要把构造函数设为私有的(至少也是受保护的),并且需要提供一个对象以及访问这个对象的静态方法。

1.饿汉式单例模式

public class Test {
    //在类的内部实例化一个实例
    private static Test test=new Test();
    //私有的构造函数,外界不能访问
    private Test(){}
    //对外提供的获取实例的静态方法
    public static Test getTest(){
        return test;
    }
}
@Test
public static void main(String[] agrs){
    Test test1=Test.getTest();
    Test test2=Test.getTest();
    System.out.println(test1==test2);//true
}

什么是饿汉式?饿汉式就是对于一个饿汉来说,他想要获得这个实例时,就能够立即拿到这个实例,而不需要等待时间。所以这里通过static的静态初始化方式,在这个类被第一次加载出来的时候,就有一个Test的实例创建出来。这就保证了第一次想要这个实例时,他已经被初始化好了。

同时,这个实例在类加载时就已经创建出来了,避免了线程安全问题。

当然,还有一种饿汉式单例模式的变种。

public class Test{
    private static Test test;
    static{
        test=new Test();
    }
    private Test(){}
    public static Test getTest(){
        return test;
    }
}

由于饿汉式单例模式是在类加载时就已经创建出来了,如果我们不需要这个实例,就会造成一个不必要的消耗。而且如果类被多次加载也会进行多次实例化。那如何在需要这个对象时,才进行实例化对象呢?这里有两个解决方式,一种是静态内部类式 ,另外一种是懒汉式

2.静态内部类式

public class Test{
    private	static class TestHolder{
        private static final Test Instance=new Test();
    }
    private Test(){}
    public static final Test getInstance(){
        return TestHolder.Instance;
    } 
}

这里同样利用 classloder加载机制保证了初始化时只有一个线程,它与饿汉式不同的是:静态内部类式实现了lazy loading,因为TestHolder类没有主动被使用,只有显示的通过调用getInstance方法才能显示装载TestHolder,从而实例化Instance。这种方法的使用场景是:当实例化Instance很耗资源,想让它延迟加载,另外一方面,不希望再Test类加载的时候就实例化Instance,这个时候这种方式就比饿汉式合理的多。

3.懒汉式

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

上面这种方法叫做懒汉单例模式,这种模式不会提前将实例创建出来,而是将类对自己的实例化延迟到第一次调用 getInstance静态方法时。这种方法存在一个问题——线程安全,当多个线程同时进入if语句时,这样就可能创建了两个不同的对象。怎么解决呢?通过给对象加锁即可。

4.线程安全的懒汉式

4.1 synchronized同步方法

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

这种方法实现线程安全,但是效率却非常低下,因为这里使用synchronized加锁的范围是整个方法,整个方法中的操作都是同步进行的,对非第一次进入这个静态方法时,不需要进入if语句中,根本不需要进行同步操作,所以就造成了效率低下。

4.2 synchronized同步代码块(双重校验锁)

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

这种实现方式缩小了锁的范围,大大提高了效率,但这种方法仍有一些隐患。

隐患的主要原因如下:

线程A发现变量没有被初始化, 然后它获取锁并开始变量的初始化。

由于某些编程语言的语义,编译器生成的代码允许在线程A执行完变量的初始化之前,更新变量并将其指向部分初始化的对象。

线程B发现共享变量已经被初始化,并返回变量。由于线程B确信变量已被初始化,它没有获取锁。如果在A完成初始化之前共享变量对B可见(这是由于A没有完成初始化或者因为一些初始化的值还没有穿过B使用的内存(缓存一致性)),程序很可能会崩溃。

解决方法:

1.使用volatile

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

上面这种方法解决前面的问题,但这种方式还存在一些问题,就是在序列化的时候。

2.使用final

class FinalWrapper<T> {
    public final T value;

    public FinalWrapper(T value) {
        this.value = value;
    }
}

public class FinalSingleton {
    private FinalWrapper<FinalSingleton> helperWrapper = null;

    public FinalSingleton getHelper() {
        FinalWrapper<FinalSingleton> wrapper = helperWrapper;

        if (wrapper == null) {
            synchronized (this) {
                if (helperWrapper == null) {
                    helperWrapper = new FinalWrapper<FinalSingleton>(new FinalSingleton());
                }
                wrapper = helperWrapper;
            }
        }
        return wrapper.value;
    }
}

5.枚举式

在1.5之前,实现单例一般只有以上几种办法,在1.5之后,还有另外一种实现单例的方式,那就是使用枚举

public enum  Singleton {

    INSTANCE;
    Singleton() {
    }
}

这个实现方式既能解决多线程安全问题,还能防止反序列化重新创建新的对象。这种方法很少见。

四.单例与序列化

序列化会破坏单例。要防止单例被序列化破坏,只要在单例类中定义readResolve即可。

public class Test{
    private static volatile Test instance;
    private Test(){}
    public static Test getInstance(){
        if(instance==null){
            synchronized(Test.class){
                if(instance==null){
                    instance=new Test();
                }
            }
        }
        return instance;
    }
    private Object readResolve() {
        return instance;
    }
}
点赞

发表评论

昵称和uid可以选填一个,填邮箱必填(留言回复后将会发邮件给你)
tips:输入uid可以快速获得你的昵称和头像

open

该博客目前已迁移到另外一个站点链接——http://blog.datealive.top/。需要更换友链请前往此站进行交换,望谅解