Android 深入理解单例模式

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Android 深入理解单例模式相关的知识,希望对你有一定的参考价值。

参考技术A

本文主要记录使用单例模式的几种形式,并分析各自的优缺点。使用单例模式可以避免重复创建对象,以此来节省开销,首先了解一下单例模式的四大原则:

常用的单例模式有:饿汉模式、懒汉模式、双重锁懒汉模式、静态内部类模式、枚举模式,我们来逐个解释这些模式的区别。

关于 volatile 修饰符,又是一个内容,需要理解:

参考(有例子,比较好理解): https://www.cnblogs.com/blog-Aevin/p/9302678.html , https://www.jianshu.com/p/ccfe24b63d87

静态内部类单例模式的优点:

那么有人会问了,如果有多个线程同时访问 getInstance() 方法,会多次初始化类,然后创建多个对象吗?答案是不会的,这我们需要了解一下类的加载机制:

虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。

所以如果一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但线程唤醒之后不会再次进入<clinit>()方法。因为在同一个加载器下,一个类只会初始化一次。)

所以静态内部类单例模式不仅能保证线程的安全性、实例的唯一性、还延迟了单例的实例化。

但是静态内部类单例模式也有一个 缺点 ,就是无法传递参数。因为它是通过静态内部类的形式去创建单例的,所以外部就无法传递参数进去。

枚举单例模式占用的内存是静态变量的两倍,所以一般都不使用enum来实现单例。

单例有饿汉模式、懒汉模式、双重锁懒汉模式、静态内部类模式、枚举模式这几种形式。

饿汉模式在初始化类时就创建了对象,容易造成资源浪费;懒汉模式在多线程环境下有风险;枚举模式占用内存过高。这三种模式都有明显的弊端,所以一般不去采用。

双重锁懒汉模式使用了 volatile 修饰符,在性能上会差一点点;静态内部类模式无法传递参数。但是这两种方式都能保证实例的唯一性,线程的安全性,也不会造成资源的浪费。所以我们在使用单例模式时,可以在这两种方式中酌情选择。

参考文章: https://blog.csdn.net/mnb65482/article/details/80458571

深入理解设计模式-单例模式

深入理解设计模式-双锁单例模式


一、什么是单例模式

单例模式是一种常用的软件设计模式,其定义是单例对象的类只能允许一个实例存在。
许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。

在这里插入图片描述


二、应用场景

举一个例子,网站的计数器,一般也是采用单例模式实现,如果你存在多个计数器,每一个用户的访问都刷新计数器的值,这样的话你的实计数的值是难以同步的。但是如果采用单例模式实现就不会存在这样的问题,而且还可以避免线程安全问题。同样多线程的线程池的设计一般也是采用单例模式,这是由于线程池需要方便对池中的线程进行控制 同样,对于一些应用程序的日志应用,或者web开发中读取配置文件都适合使用单例模式,如HttpApplication 就是单例的典型应用。 从上述的例子中我们可以总结出适合使用单例模式的场景和优缺点: 适用场景: 1.需要生成唯一序列的环境 2.需要频繁实例化然后销毁的对象。 3.创建对象时耗时过多或者耗资源过多,但又经常用到的对象。 4.方便资源相互通信的环境

三、优缺点

优点:
1.在内存中只有一个对象,节省内存空间;
2.避免频繁的创建销毁对象,可以提高性能;
3.避免对共享资源的多重占用,简化访问;
4.为整个系统提供一个全局访问点。
缺点:
1.不适用于变化频繁的对象;
2.滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为的单例类,可能会导致共 享连接池对象的程序过多而出现连接池溢出;
4.如果实例化的对象长时间不被利用,系统会认为该对象是垃圾而被回收,这可能会导致对象状态的丢失;


四、代码实现

饿汉单例模式

public class Singleton1 {
    private static Singleton1 instance = new Singleton1();

    private Singleton1(){}

    public static Singleton1 getInstance(){
        return instance;
    }
}

类加载的方式是按需加载,且加载一次。。因此,在上述单例类被加载时,就会实例化一个对象并交给自己的引用,供系统使用;而且,由于这个类在整个生命周期中只会被加载一次,因此只会创建一个实例,即能够充分保证单例,这种写法比较简单,就是在类装载的时候就完成实例化。避免了线程同步问题。

懒汉单例模式

public class Singleton2 {
    private static Singleton2 instance;

    private Singleton2(){}

    public static Singleton2 getInstance(){
        if (instance == null) {
            instance = new Singleton2();
        }
        return instance;
    }
}

我们从懒汉式单例可以看到,单例实例被延迟加载,即只有在真正使用的时候才会实例化一个对象并交给自己的引用。
这种写法起到了Lazy Loading的效果,但是只能在单线程下使用。如果在多线程下,一个线程进入了if (singleton == null)判断语句块,还未来得及往下执行,另一个线程也通过了这个判断语句,这时便会产生多个实例。所以在多线程环境下不可使用这种方式。

双锁单例模式

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

双锁模式,进行了两次的判断,第一次是为了避免不要的实例,第二次是为了进行同步,避免多线程问题。由于singleton=new Singleton()对象的创建在JVM中可能会进行重排序,在多线程访问下存在风险,使用volatile修饰signleton实例变量有效,解决该问题。


总结

单例模式的实现方法还有很多。上述是比较经典的实现方式,也是我们应该掌握的几种实现方式。
从这四种实现中,我们可以总结出,要想实现效率高的线程安全的单例,我们必须注意以下两点:
1.尽量减少同步块的作用域;
2.尽量使用细粒度的锁。

以上是关于Android 深入理解单例模式的主要内容,如果未能解决你的问题,请参考以下文章

深入理解单例设计模式

深入理解设计模式-单例模式

深入单例模式和深入理解CAS模式

深入理解单例模式

深入理解JavaScript系列(25):设计模式之单例模式

深入理解 sync.Once:单例模式的绝佳选择