一、设计模式之单例模式

一、介绍:

单例模式是应用最广的模式之一;在应用这个模式时,单例对象的类必须保证只有一个实例的存在;许多时候,整个系统只需要拥有一个全局对象,这样有利于我们协调系统整体的行为;如在一个应用中,应该只有一个ImageLoader实例,这个ImageLoder中有含有线程池、缓存系统、网络请求等,很消耗资源,很消耗资源,因此,没有理由让他构造多个实例。这种不能自由构造对象的情况,其实就是单例模式的使用场景;

二、定义

当前进程确保一个类只有一个实例,而且自行实例化并向整个系统提供这个实例;

三、单例模式使用的场景

确保某个类有且只有一个对象的场景,避免产生多个对象消耗过多的资源,或者某种类型的对象只应该有且只有一个。例如:创建一个对象需要消耗的资源过多,如要访问IO和数据库等资源,这时就要考虑使用单例模式;

四、单例模式的关键点

1、类的构造函数不对外开放,一般为Private;
2、通过一个静态方法或者枚举返回单例类对象;
3、确保单例类对象有且只有一个,特别是在多线程的环境下;
4、确保单例类对象在反序列化时不会重新构建对象;

单例模式的七种写法

  • 饿汉式(线程安全)

类加载的时候就进行了初始化,容易浪费内存,它基于classloader 机制避免了多线程的同步问题!非懒加载

public class Singleton implements Serializable {

    private static Singleton instance = new Singleton() ;

    private Singleton(){}

    public static Singleton getInstance(){
        return instance ;
    }
    //防止单例对象在反序列化时重新生成对象
    private Object readResolve() throws ObjectStreamException {
        return instance ;
    }
}
object Singleton : Serializable  {
    fun doSomething(){
        println("do something")
    }
    //防止单例对象在反序列化时重新生成对象
    private fun readResolve():Any{
        return Singleton
    }
}
  • 懒汉式(线程不安全)

最简单的单例实现,为懒加载实现, 但不支持多线程,容易造成线程不安全。因为没有加锁,严格来说不算单例!

/**
 * 懒汉式 线程不安全
*/    
public class Singleton {
    private static Singleton instance ;
    
    private Singleton(){}
    
    public static Singleton getInstance(){
        if (instance == null){
            instance = new Singleton();
        }
        return instance ;
    }
}
//懒汉式: 线程不安全
class Singleton private constructor() {
    companion object{
        private var mInstance : Singleton? = null
        get() {
            return field?: Singleton()
        }
        @JvmStatic
        fun getInstance() : Singleton{
            return requireNotNull(mInstance)
        }
    }
    fun doSomething(){
        println("do something")
    }
}
  • 懒汉式(方法加锁,线程安全)

在上一种实现方式上,在获取单例的方法上加锁 synchronized关键字,保证单例的实现,是懒加载实现,能够很好的在多线程中工作,第一次调用才初始化,避免内存浪费,但是效率很低,因为方法加锁会影响效率!

/**
 * 懒汉式 方法加锁 线程安全
*/
public class Singleton {
    private static Singleton instance ;

    private Singleton(){}

    public static synchronized Singleton getInstance(){
        if (instance == null){
            instance = new Singleton();
        }
        return instance ;
    }
}
//懒汉式,方法加锁的懒汉式,线程安全
class Singleton private constructor() : Serializable {
    companion object {
        private var mInstance : Singleton ? = null
        get() {
            return field?: Singleton()
        }
        @JvmStatic
        @Synchronized       //添加同步锁
        fun getInstance() : Singleton {
            return requireNotNull(mInstance)
        }
    }
    //防止单例对象在反序列化时生成新的对象
    private fun readResolve():Any{
        return Singleton.getInstance()
    }
    
    fun doSomething(){
        println("do something")
    }
    //kotlin调用
    fun test(){
        Singleton.getInstance().doSomething()
    }
}
  • 懒汉式(双重校验锁,DCL double-check locking ,线程安全)

懒加载 ,采用双锁检查机制,避免在对象实例时,对象实例指令发生重排,造成对象空指针。在多线程下保存高性能,单例对象需要使用volatile关键字声明,volatile关键字是线程同步的轻量级实现,能保证数据的可见性,但不能保证数据的原子性。可在实例域需要延迟化使用。

/**
 *  懒汉式 DCL 线程安全
 */    
public class Singleton {
        //volatile 修饰变量,防止指令重排
    private static volatile Singleton instance ;

    private Singleton(){}

    public static  Singleton getInstance(){
        if (instance == null){
            synchronized (Singleton.class){
                if (instance == null){
                    instance = new Singleton();         
                }
            }
        }
        return instance ;
    }
}
//懒汉式: DCL 线程安全
class Singleton private constructor(){
    companion object{
        //使用lazy属性代理,并指定LazyThreadSafetyMode为synchronized模式保证线程安全
        @JvmStatic
        val instance : Singleton by lazy(LazyThreadSafetyMode.SYNCHRONIZED) {
            Singleton()
        }
    }
    fun doSomething(){
        println("do something")
    }
}
//调用
fun test(){
    Singleton.instance.doSomething()
}

  • 静态内部类

能达到和DCL 一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是DCL。此种方式同样利用classloader机制来保证初始化单例只有一个线程,它和饿汉式不同的是:饿汉式只要类被装载了,那么instance 就会被实例化,而静态内部类实现单例是类被装载了,但instance不一定被初始化。因为内部类没有别主动使用,只有通过getInstance()调用时,才会显示装载内部类,从而实例instance。 实现时考虑:想让单例延迟加载,又不希望单例类加载时就实例化。

/**
 * 静态内部类
 */
public class Singleton {
    
    private Singleton(){}
    
    private static class SingletonHolder{
        private static final Singleton instance = new Singleton() ;
    }
    
    public static Singleton getInstance(){
        return SingletonHolder.instance;
    }
}
//静态内部类实现单例
class Singleton private constructor() {
    companion object{
        @JvmStatic
        fun getInstance(): Singleton {
            return SingletonHolder.mInstance 
        }
    }
    fun doSomething(){
        println("do something")
    }
    //静态内部类
    private object SingletonHolder {
        val mInstance = Singleton()
    }
}
  • CAS 模式

算是 懒汉式加锁 的一个变种, synchronized 是一种悲观锁, 而 CAS 是乐观锁,相对较轻,更轻量级。

import java.util.concurrent.atomic.AtomicReference;

/**
 *  CAS模式 : 存在忙等的问题,可能会造成 CPU 资源的浪费
 */
public class Singleton {

    private static AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>() ;

    private Singleton(){}

    public static final Singleton getInstance(){
           while (true){
               Singleton instance = INSTANCE.get() ;
               if (null == instance){
                   INSTANCE.compareAndSet(null,new Singleton());
               }
               return INSTANCE.get() ;
           }
    }
}
  • 枚举实现

是多线程安全,非懒加载。没有被广泛使用。它很简介,自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化

/**
 *  枚举实现单例
 */
public enum Singleton {
    //定义一个枚举,代表Singleton的一个实例
    INSTANCE ; 
    
    public void doSomething(){
        System.out.println("do something");
    }
    //假设在外部调用
    void test(){
        Singleton.INSTANCE.doSomething();
    }
}

//枚举实现单例
enum class Singleton {
    INSTANCE ;
    fun doSomething(){
        println("do something")
    }
}

总结

单例模式是使用频率很高的模式,但是,由于在客户端通常没有高并发的情况,因此,选择哪种实现方法并不会有太大的影响,即使如此,出于效率考虑,一般也是使用DCL方式和内部类单例的实现形式;
优点
1、单例模式在内存中只有一个实例,减少了内存开支,特别是一个对象需要频繁的创建和销毁时,而且创建和销毁的性能又无法优化,单例模式的优势就非常明显;
2、单例模式只生产一个实例,减少了系统的新能开销,当一个对象的场所需要较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决;
3、单例模式可以避免对资源的多重占用,例如一个写文件操作,由于只有一个实例存在内存中,避免对一个资源文件的同时写操作;
4、单例模式可以在系统设置全局的访问点,优化和共享资源访问,例如,可以设计一个单例类,负责所有的数据表的映射管理;
缺点
1、单例模式一般没有借口,扩展困难,如要扩展,除了修改代码基本上没有第二种途径可以实现;
2、单例对象如果持有Context,那么很容易引发内存泄漏,此时需要注意传给到单例对象的Context最好是Application Context ;
3、不利于测试,与单一职责原则有冲突

什么时候使用?

  • 比如生成唯一序列号的环境
  • 整个项目中需要一个共享的访问点或共享数据
  • 创建一个对象需要消耗的资源过多
  • 需要定义大量的静态常量和静态方法(如工具类)的环境

补充

volatile 关键字 : 该关键字与内存模型有关,需要先了解内存模型
计算机在执行程序时,每条指令都是在CPU中执行的,而执行过程中,需要进行数据的读取和写入。程序运行时的临时数据是存放在主存中(物理内存中),这就存在一个问题: 由于CPU执行速度很快,而从内存读取和写入数据的过程跟CPU执行指令的速度慢的多,因此如果任何时候对数据的操作都需要同内存进行交互,会大大降低指令执行的速,所以在CPU中有了高速缓存。这样,当程序在运行过程中,会将运算需要的数据从主存复制一份到 高速缓存中,这样CPU进行计算时,就可以直接从高速缓存中读取和写入数据,当运算结束后,再将高速缓存的数据刷新到主存。
多核CPU中,每条线程可能运行在不同的CPU中,因此每个线程都有自己的高速缓存,因此,对于一个变量,在多线程运行中,可能在多个CPU中都有改变量的高速缓存,这样对该该变量就有可能出现缓存不一致的问题.
并发编程中的三个问题: 原子性问题、可见性问题、有序性问题。
原子性:即一个操作或者多个操作,要么全部执行并且执行的过程不会被任何因素打断,要么就不执行。

在java中,对基本数据类型的变量的读取和赋值操作是原子性,要么执行,要么不执行。只有简单的读取、赋值才是原子操作(变量之间的操作就不是原子操作)。如果实现更大范围的原子性,可以通过synchronizeLock实现,能够保证任一时刻只能由一个线程执行该代码块,这样就不存在原子性问题了。

可见性:当多个线程访问一个变量时,一个线程修改了这个变量的值,其他线程能够立即看的到修改的值。

Java 提供了volatile关键字来保证可见性,当一个变量被其修饰时,它会保存修改立即更新到主存,当其他线程需要获取时,它会去主存中读取新值。 synchronizedLock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁后将变量的修改刷新到主存,因此可以保证可见性。

有序性:即程序执行的顺序按照代码的先后顺序执行。

Java可以通过volatile关键字来保证一定的有序性synchronizedLock也可保证有序性

指令重排:一般来说,处理器为了提高程序的执行效率,可能对输入的代码进行优化,它不保证程序中的各个语句的执行顺序和代码中的顺序一致,但它会保证程序最终的执行结果和代码顺序执行的记过是一致的。
要想并发程序正确的执行,必须保证原子性、可见性、有序性,只要一个没有被保证,就有可能导致程序运行不正确。

Java内存模型具备一些先天的有序性,即不需要通过任何手段就能够得到保证的有序性,这个通常也称为 happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。
happens-before原则:

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  • 锁定规则:一个unLock操作先行发生于后面对同一个锁的lock操作
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始

volatile:一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义: 1) 保证了不同线程对这个变量进行操作的可见性,即一个线程改变了某个变量的值,则新值对其他线程来说时立即可见的。 2) 禁止指令重排序。
最终结果:volatile可以保证操作的可见性、有序性,但不能保证操作变量的原子性。
volatile关键字参考Java并发编程:volatile关键字解析 - Matrix海子 - 博客园 (cnblogs.com)

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,294评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,493评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,790评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,595评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,718评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,906评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,053评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,797评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,250评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,570评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,711评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,388评论 4 332
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,018评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,796评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,023评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,461评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,595评论 2 350