我要做 Android 之单例模式

简介: Q:实现单例模式有几种方法?懒汉式中双层锁的目的是什么?两次判空的目的又是什么?懒汉式(线程不安全)单例模式最后的目的无非就是获取当前存在的实例对象,如果没有实例对象就实例化一个,有就直接返回。

Q:实现单例模式有几种方法?懒汉式中双层锁的目的是什么?两次判空的目的又是什么?

懒汉式(线程不安全)

单例模式最后的目的无非就是获取当前存在的实例对象,如果没有实例对象就实例化一个,有就直接返回。所谓懒汉式,可以从名字进行理解,就是在你第一次使用这个实例之前我都(懒得)不去进行实例化,一直等到第一次需要使用到这个单例的地方再(迫不得已)实例化。

提到单例模式,大多数开发者会直接写出下面的代码:

public class SingleTon {

    private static SingleTon instance;

    private SingleTon(){

    }

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

而上面的代码也就是懒汉式的单例模式,只有在第一次调用了getInstance方法的地方才会对instance进行实例化。

这种单例模式的写法在单线程的时候没有问题,但需要注意的是,在 多线程的情况下,这种单例模式的写法可能会出现实例化多个单例的情况,即这种懒汉模式是线程不安全的,所以一般情况下也是不推荐这么写单例的。

懒汉式(线程安全)

像多数线程不安全的问题的解决方法相同,实现线程安全的懒汉单例进行加锁即可,例如直接将锁加在上面的例子里获取单例的静态方法getInstance上:

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

显然,这种方法可以解决线程不安全的问题,即使多线程并行也只会存在一个单例,但这种方法的缺点是效率太低,仔细想想,其实有必要进行加锁的只有第一次判断instance为空而进行实例化的时候,而在instance实例化完成后,后面无论是哪个线程读取到的instance都已经不为null,可以直接将当前instance返回。而这种对整个方法进行加锁的操作则会使得每一次获取单例都进行加锁,效率十分低下。

双重检验锁

针对上面提出的问题,出现了第三种单例模式的写法,即双重检验锁的单例模式:

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

所谓双层检验锁,即像上面的例子中所示的,在加锁前后对实例对象进行两次判空的检验,加锁是为了第一次对象实例化的线程同步,而锁内还要有第二层判空是因为可能会有多个线程进入第一层if判断内部,而在加锁代码块外排队等候,如果锁内不进行第二次检验,仍然会出现实例化多个对象的情况。

而进行了双重检验之后,除了第一次实例化需要进行加锁同步,之后的线程只要进行第一层的if判断不为空即可直接返回,而不用每一次获取单例都加锁同步,因此相比前面两种单例模式的写法,更推荐用这种双层检验锁的方式来写单例。

然而,双层检验锁也不是完美的,问题出现在对单例对象进行实例化的过程中,在执行instance = new SingleTon()这句代码时大概做了三件事:

  1. 给instance分配内存空间
  2. 通过调用SingleTon的构造函数初始化成员变量
  3. 将instance这个引用指向先前分配好的内存空间

JVM在执行编译的时候实际上不能保证上述三件事中2和3的执行顺序,也就是说可能发生的执行顺序是123或132,如果是123的执行顺序则不会发生问题,而如果是132的执行顺序,则instance作为一个引用,在指向分配好的内存空间的时候本身就已经不为null了,此时如果有另一条线程调用getInstance方法,则会直接将还没有对成员变量完成初始化的instance返回,程序就可能发生崩溃。

为了解决这个问题,我们加入了violate关键字:

private static violate SingleTon instance;

violate的作用可以理解为一个读写锁,使用violate修饰后,一旦开始对instance进行操作,则只能将前面所说的三步全部进行完之后才能再读取instance,有了violate关键字,才能解决上面提到的问题。

饿汉式

可以看到,前面说的所有问题的根源都出在多线程上,如果可以解决多线程同步的问题,那么前面的问题也就迎刃而解了,于是就出现了饿汉式的单例模式。

对比懒汉式,懒汉是懒得进行实例化,直到要用到这个单例的时候才进行实例化,那么饿汉就是饥饿难耐、等不及的就进行了实例化,直接来看代码:

public class SingleTon {

    private static SingleTon instance = new SingleTon();

    private SingleTon(){

    }

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

显然,由于被修饰为static变量,instance会在类第一次加载到内存中时就进行了实例化,由于类加载机制的特性,这种写法本身就是线程安全的,所以不会有前面三种方式中存在的问题。

由于static修饰,instance会在类第一次被加载到内存中时就被初始化,如果某次运行期间并没有用到这个单例,就浪费了资源去加载了这个对象,不能做到延时加载;另一方面,有些时候我们需要依赖其他类创建的资源来创建这个单例,即需要在我们第一次调用getInstance方法的时候才真正的创建单例对象,这种一开始就初始化完成的方式也无法满足这种需求。

静态内部类式

针对上面饿汉式方法存在的问题,能不能有一种既能延迟加载、又能解决同步问题的单例模式写法呢?这便需要用静态内部类的方式了。

public class SingleTon {

    private static class SingleTonInstance{
        private static SingleTon instance = new SingleTon();
    }

    private SingleTon(){

    }

    public static SingleTon getInstance(){
        return SingleTonInstance.instance;
    }
}

由于静态内部类是私有的,因此除了getInstance方法没有其他方式能访问到它,因此只有在getInstance调用时才能真正创建单例,实现了单例创建的延迟,同时由于静态类只会加载一次的特性,因此也不会出现线程同步问题

  • 都需要额外的工作实现序列化,否则每次反序列化一个序列化对象时都会创建一个新的实例;
  • 仍然可以使用反射强行调用私有的构造函数构建单例。

枚举

实际上,在Java中最优雅的单例写法是使用枚举。

public enum  EnumSingle {
    INSTANCE;
}

简单的一个枚举,实际上就解决了线程同步、防止反射调用构造函数的问题,另一方面,枚举提供了自动序列化机制,防止了反序列化的时候重新创建对象,因此这是新版effective Java最推荐的单例写法。

注意

前面说枚举是最推荐的单例写法,从理论上来说,同时解决了前面所说的同步问题、反射调用构造函数问题、反序列化重新创建对象问题的枚举写法确实是最优雅的单例模式,但是需要注意的是,枚举类并不是普通类,使用枚举类对象作为单例自然丢弃了一些普通类的特性(继承等),而且枚举和饿汉式写法一样,无法实现延迟加载。

所以综上所述,如果不需要延迟加载,可以优先使用枚举实现单例;如果需要延迟加载,可以优先使用静态内部类。

当然,上面的分析是针对Java来说的,但是实际上在Android中因为性能问题不推荐使用枚举,而且Android中使用的Java都是在1.5版本以上的,所以Android使用的单例推荐使用静态内部类或者双重检验锁。

目录
相关文章
|
设计模式 SQL 缓存
Android 单例模式必知必会
一、概念 单例模式是运用最广泛的设计模式之一,在应用这个模式时,单例模式的类必须保证只有一个实例存在。多用于整个程序只需要有一个实例,通常很消耗资源的类,比如线程池,缓存,网络请求,IO操作,访问数据库等。由于类比较耗资源,所以没必要让它构造多个实例,这种就是单例模式比较好的使用场景。
204 0
|
安全 Java 缓存
Android--单例模式
版权声明:本文为博主原创文章,转载请标明出处。 https://blog.csdn.net/chaoyu168/article/details/78897883 ###定义 保证一个类仅有一个实例,并提供一个访问它的全局访问点。
837 0
|
API Android开发
Android设计模式系列-单例模式
版权声明:本文为博主原创文章,转载请标明出处。 https://blog.csdn.net/chaoyu168/article/details/52592563 单例模式,可以说是GOF的23种设计模式中最简单的一个。
931 0
|
API 开发工具 Android开发
Android设计模式系列(3)--SDK源码之单例模式
原文链接:http://www.cnblogs.com/qianxudetianxia/archive/2011/08/07/2130306.html 单例模式,可以说是GOF的23种设计模式中最简单的一个。
1004 0
|
3天前
|
搜索推荐 Android开发 开发者
探索安卓开发中的自定义视图:打造个性化UI组件
【10月更文挑战第39天】在安卓开发的世界中,自定义视图是实现独特界面设计的关键。本文将引导你理解自定义视图的概念、创建流程,以及如何通过它们增强应用的用户体验。我们将从基础出发,逐步深入,最终让你能够自信地设计和实现专属的UI组件。
|
5天前
|
Android开发 Swift iOS开发
探索安卓与iOS开发的差异和挑战
【10月更文挑战第37天】在移动应用开发的广阔舞台上,安卓和iOS这两大操作系统扮演着主角。它们各自拥有独特的特性、优势以及面临的开发挑战。本文将深入探讨这两个平台在开发过程中的主要差异,从编程语言到用户界面设计,再到市场分布的不同影响,旨在为开发者提供一个全面的视角,帮助他们更好地理解并应对在不同平台上进行应用开发时可能遇到的难题和机遇。
|
7天前
|
XML 存储 Java
探索安卓开发之旅:从新手到专家
【10月更文挑战第35天】在数字化时代,安卓应用的开发成为了一个热门话题。本文旨在通过浅显易懂的语言,带领初学者了解安卓开发的基础知识,同时为有一定经验的开发者提供进阶技巧。我们将一起探讨如何从零开始构建第一个安卓应用,并逐步深入到性能优化和高级功能的实现。无论你是编程新手还是希望提升技能的开发者,这篇文章都将为你提供有价值的指导和灵感。
|
5天前
|
存储 API 开发工具
探索安卓开发:从基础到进阶
【10月更文挑战第37天】在这篇文章中,我们将一起探索安卓开发的奥秘。无论你是初学者还是有经验的开发者,这篇文章都将为你提供有价值的信息和建议。我们将从安卓开发的基础开始,逐步深入到更复杂的主题,如自定义组件、性能优化等。最后,我们将通过一个代码示例来展示如何实现一个简单的安卓应用。让我们一起开始吧!
|
6天前
|
存储 XML JSON
探索安卓开发:从新手到专家的旅程
【10月更文挑战第36天】在这篇文章中,我们将一起踏上一段激动人心的旅程,从零基础开始,逐步深入安卓开发的奥秘。无论你是编程新手,还是希望扩展技能的老手,这里都有适合你的知识宝藏等待发掘。通过实际的代码示例和深入浅出的解释,我们将解锁安卓开发的关键技能,让你能够构建自己的应用程序,甚至贡献于开源社区。准备好了吗?让我们开始吧!
18 2