侧边栏壁纸
博主头像
qingtian博主等级

喜欢是一件细水流长的事,是永不疲惫的双向奔赴~!

  • 累计撰写 104 篇文章
  • 累计创建 48 个标签
  • 累计收到 1 条评论

单例模式

qingtian
2020-09-08 / 0 评论 / 1 点赞 / 1,297 阅读 / 5,728 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2020-11-09,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

单例模式

确保一个类只有一个实例,并提供该实例的全局访问点。

用途

单例模式有以下两个优点:

在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如网站首页页面缓存)。

避免对资源的多重占用(比如写文件操作)。

实现方式

我们知道,一个类的对象的产生是由类构造函数来完成的。如果一个类对外提供了public的构造方法,那么外界就可以任意创建该类的对象。所以,如果想限制对象的产生,一个办法就是将构造函数变为私有的(至少是受保护的),使外面的类不能通过引用来产生对象。同时为了保证类的可用性,就必须提供一个自己的对象以及访问这个对象的静态方法。

QQ20160406-0

饿汉式

下面是一个简单的单例的实现:

public class Singleton {
    //在类内部实例化一个实例
    private static Singleton instance = new Singleton();
    //私有的构造函数,外部无法访问
    private Singleton() {
    }
    //对外提供获取实例的静态方法
    public static Singleton getInstance() {
        return instance;
    }
}

通过static的静态初始化方式,在该类第一次被加载的时候,就有一个SimpleSingleton的实例被创建出来了。这样就保证在第一次想要使用该对象时,他已经被初始化好了。

饿汉模式 - 变种

public class Singleton2 {
    //在类内部定义
    private static Singleton2 instance;
    static {
        //实例化该实例
        instance = new Singleton2();
    }
    //私有的构造函数,外部无法访问
    private Singleton2() {
    }
    //对外提供获取实例的静态方法
    public static Singleton2 getInstance() {
        return instance;
    }
}

饿汉式单例,在类被加载的时候对象就会实例化。这也许会造成不必要的消耗,因为有可能这个实例根本就不会被用到。而且,如果这个类被多次加载的话也会造成多次实例化。其实解决这个问题的方式有很多,下面提供两种解决方式,第一种是使用静态内部类的形式。第二种是使用懒汉式。

静态内部内方式

public class StaticInnerClassSingleton {
    //在静态内部类中初始化实例对象
    private static class SingletonHolder {
        private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
    }
    //私有的构造方法
    private StaticInnerClassSingleton() {
    }
    //对外提供获取实例的静态方法
    public static final StaticInnerClassSingleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

这种方式同样利用了classloder的机制来保证初始化instance时只有一个线程,它跟饿汉式不同的是(很细微的差别):饿汉式是只要Singleton类被装载了,那么instance就会被实例化(没有达到lazy loading效果),而这种方式是Singleton类被装载了,instance不一定被初始化。因为SingletonHolder类没有被主动使用,只有显示通过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。想象一下,如果实例化instance很消耗资源,我想让他延迟加载,另外一方面,我不希望在Singleton类加载时就实例化,因为我不能确保Singleton类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化instance显然是不合适的。这个时候,这种方式相比饿汉式更加合理。

懒汉式 - 线程不安全

这种单例叫做懒汉式单例。懒汉,就是不会提前把实例创建出来,将类对自己的实例化延迟到第一次被引用的时候。getInstance方法的作用是希望该对象在第一次被使用的时候被new出来。

有没有发现,其实code 5这种懒汉式单例其实还存在一个问题,那就是线程安全问题。在多线程情况下,有可能两个线程同时进入if语句中,这样,在两个线程都从if中退出的时候就创建了两个不一样的对象。

public class Singleton {
    //定义实例
    private static Singleton instance;
    //私有构造方法
    private Singleton(){}
    //对外提供获取实例的静态方法
    public static Singleton getInstance() {
        //在对象被使用的时候才实例化
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

懒汉式 - 线程安全

public class SynchronizedSingleton {
    //定义实例
    private static SynchronizedSingleton instance;
    //私有构造方法
    private SynchronizedSingleton(){}
    //对外提供获取实例的静态方法,对该方法加锁
    public static synchronized SynchronizedSingleton getInstance() {
        //在对象被使用的时候才实例化
        if (instance == null) {
            instance = new SynchronizedSingleton();
        }
        return instance;
    }
}

以上方法虽然是线程安全的,但是它使用synchronized同步的方法,该方法的所有操作都是同步进行的,但是对于非第一次创建对象的情况,也就是没有进入if语句中的情况,根本不需要同步操作,可以直接返回instance

双重校验锁

public class Singleton {

    private static Singleton singleton;

    private Singleton() {
    }

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

以上方法,通过使用同步代码块的方式减小了锁的范围。这样可以大大提高效率。

问题:

这里我们分析创建一个对象的过程:

  1. 虚拟机遇到new指令,到常量池定位到这个类的符号引用。
  2. 检查符号引用代表的类是否被加载、解析、初始化过。
  3. 虚拟机为对象分配内存。
  4. 虚拟机将分配到的内存空间都初始化为零值。
  5. 虚拟机对对象进行必要的设置。
  6. 执行方法,成员变量进行初始化。
  7. 将对象的引用指向这个内存区域。

简化过程

  1. Jvm检查该类是否被加载过,连接,初始化过
  2. 虚拟机为对象分配内存
  3. 虚拟机将分配到的内存初始化为零值
  4. 在内存M中初始化对象
  5. 将内存M的地址赋给singleton变量

在这里第4和第5步是会由于编译器进行指令优化而发生指令重排。

由于指令重排,线程A在执行对象的初始化方法时,singleton变量获得的是未初始化完成的内存M的地址,线程B此时进行判断if (singleton == null)false,则此时线程B获得的是一个未完成初始化的对象。

在线程B继续运行时,会发生程序崩溃。

双重校验锁 - 改进

使用volatile

public class VolatileSingleton {
    private static volatile VolatileSingleton singleton;

    private VolatileSingleton() {
    }

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

使用volatile关键字,由于volatile的效果,禁止指令重排和编译器优化。

**上面这种双重校验锁的方式用的比较广泛,他解决了前面提到的所有问题。**但是,即使是这种看上去完美无缺的方式也可能存在问题,那就是遇到序列化的时候。

上面方法问题要关注序列化对单例模式的破坏

枚举式

public enum Singleton {

    INSTANCE;

    private String objName;


    public String getObjName() {
        return objName;
    }


    public void setObjName(String objName) {
        this.objName = objName;
    }


    public static void main(String[] args) {

        // 单例测试
        Singleton firstSingleton = Singleton.INSTANCE;
        firstSingleton.setObjName("firstName");
        System.out.println(firstSingleton.getObjName());
        Singleton secondSingleton = Singleton.INSTANCE;
        secondSingleton.setObjName("secondName");
        System.out.println(firstSingleton.getObjName());
        System.out.println(secondSingleton.getObjName());

        // 反射获取实例测试
        try {
            Singleton[] enumConstants = Singleton.class.getEnumConstants();
            for (Singleton enumConstant : enumConstants) {
                System.out.println(enumConstant.getObjName());
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
0

评论区