设计模式之——单例模式

一、单例模式

单例模式(Singleton Pattern)是最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

注意:

  • 1、单例类只能有一个实例。
  • 2、单例类必须自己创建自己的唯一实例。
  • 3、单例类必须给所有其他对象提供这一实例。

介绍

意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。

主要解决:一个全局使用的类频繁地创建与销毁。

何时使用:当您想控制实例数目,节省系统资源的时候。

如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。

关键代码:构造函数是私有的。

应用实例: 1、一个党只能有一个书记。 2、Windows 是多进程多线程的,在操作一个文件的时候,就不可避免地出现多个进程或线程同时操作一个文件的现象,所以所有文件的处理必须通过唯一的实例来进行。 3、一些设备管理器常常设计为单例模式,比如一个电脑有两台打印机,在输出的时候就要处理不能两台打印机打印同一个文件。

优点: 1、在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例(比如管理学院首页页面缓存)。 2、避免对资源的多重占用(比如写文件操作)。

缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

1.1 单例模式——饿汉式

​ 饿汉式,顾名思义,形容一个人饥饿到疯狂,迫不及待的要吃食物。其实饿汉式单例模式也是这样,在访问这个单例类的时候会直接创建一个单例保存在单例类中,不管你是否使用都会创建出来,这可能造成资源浪费。但是它是使用起来最简单的单例模式。

代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace 单例模式_饿汉式_静态内部类_
{
    /// <summary>
    /// 程序一但类被访问,单例就会被创建出来
    /// </summary>
    class HungrySingle
    {
        // 1. 构造器私有化
        //防止外部通过new关键字创建对象
        private HungrySingle() { }

        // 2.创建一个成员变量用来保存这个单例
        //readonly:不能修改,只能读取,和const关键字有区别
        public static readonly HungrySingle hungrySingle = new HungrySingle();

        // 3.给外部提供一个方法,用来获取单例
        public static HungrySingle GetSingle()
        {
            return HungrySingle;
        }
    }
    
}

1.2 单例模式——懒汉式

​ 上面提到,饿汉式单例模式不管你是否使用单例,只要你访问该类,这个类就会被加载,单例模式就会创建出来,造成了资源浪费。懒汉式就是为了解决资源浪费的问题产生的,它不会直接在变量中new 一个对象出来,而是在方法中创建对象,你只有通过方法获取单例才会给你创建一个单例。

代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace 单例模式
{
    /// <summary>
    /// 懒汉式
    /// 当你需要对象的时候,才会创建一个对象,不会造成资源的浪费
    /// </summary>
    class LazyManSingle
    {
        //声明一个标识,用来记录构造函数是否被调用
        private static bool isNew = false;
        //1.构造器私有化
        //防止外部通过new 关键字创建对象
        private LazyManSingle(){}
        
        //2.声明一个静态的私有变量,但不要创建对象
        private static LazyManSingle _lazyManSingle = null;

        //3.通过方法创建对象,并且返回
        public static LazyManSingle GetSingle()
        {
            //当使用的时候,进行创建对象
            if (_lazyManSingle == null)
            {
                _lazyManSingle = new LazyManSingle();
            }
            return _lazyManSingle;
        }
    }
}

​ 好了,这样资源浪费的问题被解决了,那么单例模式是不是就完美了呢?哈哈,当然不是!你还记得多线程吗?在多线程中,可能会有多个线程去通过GetSingle()来获取实例,方法执行的过程中会执行构造函数,那么你会产生疑问:我已经在GetSingle()方法中判断了,如果单例不为空就不会再创建对象了,还有啥问题。

​ 是的,这在单线程是完全可以的,但是在多线程中当有一个线程通过if判断后,刚要创建对象时,线程执行权被其他线程抢过去了,导致这个线程“暂停执行”,这个时候单例仍然为null。此时,第二个线程也可以通过if判断,这导致了这两个线程接下来都会创建一个实例。那么单例模式就被破坏了。

​ 那么怎么解决这个问题呢?我们知道,防止线程争夺资源要使用“锁(lock)”。那么就很简单了,给if的外面套上一层锁就好了。修改后代码如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace 单例模式
{
    /// <summary>
    /// 懒汉式
    /// 当你需要对象的时候,才会创建一个对象,不会造成资源的浪费
    /// </summary>
    class LazyManSingle
    {
        //声明一个标识,用来记录构造函数是否被调用
        private static bool isNew = false;
        //1.构造器私有化
        //防止外部通过new 关键字创建对象
        private LazyManSingle(){}
        
        //2.声明一个静态的私有变量,但不要创建对象
        private static LazyManSingle _lazyManSingle = null;

        //创建锁
        private static Object o = new object();
        
        //3.通过方法创建对象,并且返回
        public static LazyManSingle GetSingle()
        {
             if (_lazyManSingle == null)
             {
                 lock (o)
                 {
                     //当使用的时候,进行创建对象
                     if (_lazyManSingle == null)
                     {
                         _lazyManSingle = new LazyManSingle();
                         Console.WriteLine("我创建了一个实例");
                     }
                 }
             }
             return _lazyManSingle;
        }
    }
}

​ 细心的小伙伴们发现了,我在锁的外面又加了一层判断,看起来好像很多余。是的,其实加不加影响不大,但是加这一层判断有一个好处:防止线程等待锁的释放造成资源的浪费。如果单例模式为空才会去创建单例,这个时候会等待这把锁的释放,这要消耗一定的资源。但是如果不为空,我们直接返回单例即可,没有必要等待锁的释放了。这种方法叫做“双层检查锁”

​ 哇!头有点痒了,不会要长脑子了吧。别急,还有一个小细节,在通过new关键字创建对象的时候,会分三步走:首先会给要创建的类对象分配一块内存,然后执行构造函数初始化对象,最后将该内存的内存地址返回。这三步我们通常叫做三个指令,这三个指令的执行顺序一般来说都是不变的,但是凡事都有意外,在某些极端情况,指令的执行顺序会发生改变。如果指令按照132走,那么当指令执行到3的时候,单例就不是null了,而是一块没有意义的内存,此时其他线程会误以为单例被创建了,从而直接返回一个单例,得到了错误的结果,这就是指令重排。为了避免这种情况,我们需要给单例前添加一个关键字“volatile"

//用volatile标记的成员,可以避免指令重排
private volatile static LazyManSingle _lazyManSingle = null;

注意:单线程中指令重排没有影响,但是多线程还是要注意一下,可能会发生错误!

​ 那么直到现在,基本上单例模式就大功告成了!所谓道高一尺,魔高一丈,当你在凝视深渊的时候,深渊也在凝视你。有这么一个东西,它可以获取程序集所有类的信息,并且可以根据这些信息创建任何我想要的东西,它就是————反射。既然你的构造函数已经私有化,我无法通过常规手段进行创建对象,但是你做不到的东西反射可以做到,不管你私有不私有,反射都可以获取这个类中的构造函数并执行,达到创建对象的效果。

具体的实现如下:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Reflection;

namespace 单例模式
{
    
    class Program
    {
        static void Main(string[] args)
        {
            //通过反射来破坏单例:通过反射来获取构造函数,进行创建对象
            Type type = typeof(LazyManSingle);
            Type[] parameterTypes = new Type[0];
            //获取私有无参构造函数
            ConstructorInfo construct = type.GetConstructor(BindingFlags.NonPublic | BindingFlags.Instance, null, parameterTypes, null);
            
            //执行构造函数创建对象
            LazyManSingle lazyInstance = construct.Invoke(null) as LazyManSingle;
            LazyManSingle lazyInstance2 = construct.Invoke(null) as LazyManSingle;
            
            Console.WriteLine(lazyInstance?.GetHashCode());
            Console.WriteLine(lazyInstance2?.GetHashCode());

        }
    }
}

​ 我们也有解决方案:既然反射通过构造函数去创建对象,那么不妨在构造函数中加入一些判断,如果是第一次执行构造函数,我们不用管;如果构造函数被执行过仍然试图调用,我们抛出异常警告。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace 单例模式
{
    /// <summary>
    /// 懒汉式
    /// 当你需要对象的时候,才会创建一个对象,不会造成资源的浪费
    /// </summary>
    class LazyManSingle
    {
        //声明一个标识,用来记录构造函数是否被调用
        private static bool isNew = false;
        //1.构造器私有化
        private LazyManSingle()
        {
            lock (o)
            {
                if (isNew == false)
                {
                    isNew = true;
                }
                else
                {
                    throw new Exception("不要试图使用反射破坏单例!");
                }
            }
           
        }
    }

     //剩余代码省略了。。。。
}

​ 当第一次执行构造函数的时候,isNew被赋值为true,之后如果继续调用构造函数,此时就会抛出异常。这种方法叫做标记位,经常用来判断一个方法有没有执行过。

​ 其实,即使是这样也仍然存在缺陷,既然构造函数都能拿到,那么私有变量也不在话下。我们拿到私有变量,在每次创建对象前将isNew改为false,就逃避了检查。

​ 那么,就没有一种单例模式是完美无瑕的吗?答案是有的,这种单例模式是通过枚举实现的,比如java就可以通过枚举来实现绝对完美的单例模式,不过C#语言中的枚举和java中的枚举是非常不同的,java中的枚举是可以当作类来用的,它可以有变量和方法等,但是C#的枚举中只能有枚举值。所以C#是无法实现终极单例模式的,尽管java和C#有95%的相似,可是有时往往有那么一些偏差。

1.3 单例模式——饿汉式加强版(静态内部类)

这里我们先回顾一下前面的两种单例模式:

饿汉式

  • 优点:使用方便,代码简单,没有线程安全问题
  • 缺点:资源浪费问题,不能实现懒加载,也不能传递参数

懒汉式

  • 优点:能够实现懒加载,避免了资源浪费,使用灵活
  • 缺点:有线程安全问题

​ 可见,饿汉式的主要问题就是无法懒加载,那么我们可以通过一种方法来解决这个问题,就是静态内部类:即一个类中存在的静态类。静态内部类的特点就是不会随着外部类的加载而加载,我们把这个单例放到静态内部类中即可。

代码:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace 单例模式_饿汉式_静态内部类_
{
    /// <summary>
    /// 通过静态内部类实现懒加载
    /// 静态内部类不会因为它外面的类加载而加载
    /// </summary>
    class HungrySingle
    {
        // 1. 构造器私有化
        private HungrySingle() { }

        // 2.静态内部类声明一个实例
        private static class InnerClass
        {
            public static readonly HungrySingle hungrySingle = new HungrySingle();
        }

        // 3.给外部提供一个方法,用来获取单例
        public static HungrySingle GetSingle()
        {
            return InnerClass.hungrySingle;
        }
    }
}

​ 至此单例模式完美结束,具体使用还需要结合场景哦。

  • 24
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值