02.你真的知道线程安全的“单件模式”吗?

简介: 02.你真的知道线程安全的“单件模式”吗?


阅读目录

原文地址:02.你真的知道线程安全的“单件模式”吗?

 

概述:

  单件模式的类图可以说是所有模式的类图中最简单的,事实上,它的类图上只有一个类。

  尽管从设计的视角来说它很简单,但是实现上还是会遇到相当多的波折。

回到顶部

一、与单件模式的问答

1.单件模式只有一个类,应该是很简单的模式,但是问题似乎不少

答:固然正确地实现单件模式需要一点技巧,但是阅读完这篇文章之后,你已经具备了用正确的方式实现单件模式的能力。当你需要控制实例个数时,还是应当使用单件模式。

2.难道我不能创建一个类,把所有的方法和变量都定义为静态的,把类直接当作一个单件?

  答:如果你的类自给自足,而且不依赖于复杂的初始化,那么你可以这么做。但是,因为静态初始化的控制是在CLR受伤,这么做有可能导致混乱,特别是当有许多类牵涉其中的时候。这么做常常会造成一些微妙的,不容易发现的和初始uade次序有关的bug。除非你有绝对的必要使用类的单件,否则还是建议使用对象的单件

3.类应该做一件事,而且只做一件事。类如果能做两件事,就会被认为是不好的OO设计,单件有没有违反这样的观念?

  答:你说的是“一个类,一个责任”原则。没错,你似的对的,但见类不只负责管理自己的实例,并提供全局访问,还在应用程序中担当角色,所以也可以被视为是两个责任。尽管如此,由类管理自己的实例的做法并不少见。这可以让整体设计更简单。更何况,许多开发人员都已经熟悉了单件模式的这种做法。

4.我想把单件类当成超类,设计出子类,但是我遇到了问题,究竟可以不可以继承单件类?

  答:继承单件类会遇到一个问题,就是构造器是私有的。你不能用私有构造器来扩展类。所以你必须把单件的构造器改成公共的或受保护的。但是这么一来就不算真正的单件了,因为别的类也可以实例化他。

如果你果真把构造器的访问权限改了,还有另一个问题出现,单件的实现是利用静态变量,直接继承会导致所有的派生类共享同一个实例变量,这可能不是你想要的。

5.我还是不了解为何全局变量比单件模式差。

  答:在.net中,全局变量基本上就是对对象的静态引用。在这样的情况下使用全局变量会有一些缺点,我们已经提到了其中的一个:急切实例化VS延迟实例化。但是我们要记住这个模式的目的:确保类只有一个实例并提供全局访问,但是不能确保只有一个实例。全局变量也会变相鼓励开发人员,用许多全局变量指向许多小对象来造成这样的现象,但单件仍然可能被滥用。

回到顶部

二、垃圾回收

如果没有一个全局变量引用单件模式的实例,该实例是否会被垃圾回收?

经过自己写代码的验证:不会被回收。

由下面的结果可知,两次调用GetInstance,只创建了一次Singleton实例

图片.png

回到顶部

三、职责:

  1.保证一个类有且仅有一个实例

  2.且提供一个全局访问点

 

回到顶部

四、代码中需要用到的地方

  线程池(Thread Pool)/缓存(cache)/对话框/处理偏好设置和注册表的对象/日志对象/充当打印机/显卡等设备的驱动程序的对象。

 

回到顶部

五、生活中用到的地方

  1.考勤记录仪可以有多台,但是时钟必须只有一个,所有的考勤记录必须根据这个时钟来生成打卡时间记录。且该时钟是唯一的时间访问入口。

  2.足球场上只能根据主裁判的手表来判断比赛进行了多长时间,比赛进行的时间是唯一的,

查看比赛进行的时间的访问入口时主裁判的手表上的时间。

 

回到顶部

六、对比全局静态变量

我们可以用全局静态变量指向一个对象。程序员工作的时候约定好,只用这个全局变量作为唯一的一个访问这个对象的入口。

优点:

  1.代码定义简单

  2.调用方便

缺点:

  1.程序员之间需要约定

  2.程序一开始就得创建好对象,如果该对象非常耗资源,而程序执行的过程中又一直没有用到它,就形成了资源的浪费。

  3.不能保证一个对象只能被实例化一次,如果程序员之间的约定并没有严格遵守,比如新来的同事并不知道有这个约定。

 

 

回到顶部

七、原理图:

Singleton

Static uniqueInstance

//其他有用的单件数据...

Static GetInstance()

//其他有用的单件方法...

 

 

 

回到顶部

八、代码示例

 

1.简单实现方式:

namespace SimpleSingleton
{
    public sealed class Singleton
    {
        //定义一个静态变量,指向Singleton实例
        private static Singleton _singleton = null;
        //私有构造函数,外部不能访问
        private Singleton()
        {
            //初始化
        }
        public static Singleton GetInstance()
        {
            //如果_singleton为null,创建一个Singleton对象,并将_singleton指向Singleton对象
            if (_singleton == null)
            {
                _singleton = new Singleton();
            }
            return _singleton;
        }
    }
}

 

 

优点:

  1.在单线程的程序中,对象只会被创建一次。

  2.实例的初始化延迟到了子类中,子类中可以判断是否已存在实例而进行初始化或直接返回已经初始化的实例。这种延迟初始化避免了不必要的创建实例

缺点:

  1.这种方式对于线程来说不是安全的,如果有两个线程同时进入到if 的处理代码中,就会造成创建了两个Singleton实例

 

 

2.安全的线程实现的方式

namespace SafetySingleton
{
    public sealed class Singleton
    {
        //定义一个静态变量,指向Singleton实例
        private static Singleton _singleton = null;
        private static readonly object _padLock = new object();
        //私有构造函数,外部不能访问
        private Singleton()
        {
            //初始化
        }
        public static Singleton GetInstance()
        {
            //lock就是把一段代码定义为临界区,所谓临界区就是同一时刻只能有一个线程来操作临界区的代码,
            //当一个线程位于代码的临界区时,另一个线程不能进入临界区,如果试图进入临界区,则只能一直等待(即被阻止),
            //直到已经进入临界区的线程访问完毕,并释放锁标志。
            lock(_padLock)
            {
                //如果_singleton为null,创建一个Singleton对象,并将_singleton指向Singleton对象
                if (_singleton == null)
                {
                    _singleton = new Singleton();
                }
            }
            return _singleton;
        }
    }
}

 

 

优点:

  1.Singleton实例只会被创建一次,因为lock关键字只允许一个线程进入lock所包括的代码,阻塞其他的线程进入到lock所包括的代码中,所以实例只会被创建一次

缺点:

  1.不能实现多线程,必然降低了性能。

 

 

3.双重锁定

namespace TwiceLockSingleton
{
    public sealed class Singleton
    {
        //定义一个静态变量,指向Singleton实例
        private static Singleton _singleton = null;
        private static readonly object _padLock = new object();
        //私有构造函数,外部不能访问
        private Singleton()
        {
            //初始化
        }
        public static Singleton GetInstance()
        {
            //先判断_singleton是否为null,如果为null则创建,且创建的代码是独占式的。
            //如果_singleton不为null,则直接返回_singleton
            if (_singleton == null)
            {
                //lock就是把一段代码定义为临界区,所谓临界区就是同一时刻只能有一个线程来操作临界区的代码,
                //当一个线程位于代码的临界区时,另一个线程不能进入临界区,如果试图进入临界区,则只能一直等待(即被阻止),
                //直到已经进入临界区的线程访问完毕,并释放锁标志。
                lock (_padLock)
                {
                    //如果_singleton为null,创建一个Singleton对象,并将_singleton指向Singleton对象
                    if (_singleton == null)
                    {
                        _singleton = new Singleton();
                    }
                }
            }
            return _singleton;
        }
    }
}

 

优点:

  1.相对于方法二,性能上有提升,并不是每次都进行锁定

  2.Singleton实例只会被创建一次

缺点:

  1.该方式较复杂

  2.加了两次判断,对性能有损失

 

4.静态初始化实现方式

namespace StaticInitializeSingleton
{
    public sealed class Singleton
    {
        //定义一个静态变量,指向Singleton实例
        private static Singleton _singleton = new Singleton();
        //私有构造函数,外部不能访问
        private Singleton()
        {
            //初始化
        }
        public static Singleton GetInstance()
        {
            return _singleton;
        }
    }
}

 

优点:

  1.实现方式简单

  2.减少了判断,相比较于前面的三个例子,该实现方式在性能上有很大的提升。

缺点:

  1.由于创建实例交给了CLR公共语言运行时,所以没有实例化的控制权。

 

5.延迟初始化实现方式

namespace LazyInstantitionSingleton
{
    public sealed class Singleton
    {
        //私有构造函数,外部不能访问
        private Singleton()
        {
            //初始化
        }
        public static Singleton GetInstance()
        {
            return Nested._singleton;
        }
        public class Nested()
        {
            static Nested()
            {
            }
            internal static readonly Singleton _singleton  = new Singleton();
        }
    }
}

 

优点:

  1.创建实例延迟到了Nested类里面

回到顶部

九、总结

1.单件模式:确保一个类只有一个实例,并提供一个全局访问点

2.我们正在把某个类设计成自己管理的一个单独实例,同时也避免其他类再自行产生实例。要想取得单件实例,通过单件类是唯一的途径。

3.我们也提供这个实例的全局访问点:当你需要实例时,向类查询,它会返回单个实例。前面的例子利用延迟实例化的方式创建单件,这种做法对资源敏感的对象特别重要。

 

回到顶部

十、Demo程序

巧克力工厂

大家都知道,现代化的巧克力工厂具备计算机控制的巧克力锅炉。锅炉做的事,就是把巧克力和牛奶融在一起,然后送到下一阶段,以制造巧克力棒。

这里有一个巧克力公司的工业强度巧克力锅炉控制器。看看它的代码,你会发现大妈写得相当消息,他们在努力防止不好的事情发生。例如,锅炉已经满了还继续放原料。

1.巧克力工厂类

using System;
namespace SingletonPattern
{
    public class ChocolateBoiler
    {
        private Boolean empty;
        public ChocolateBoiler()
        {
            empty = true;
        }
        public void fill()
        {
            if (isEmpty())
            {
                Console.WriteLine("Fill");
                empty = false;
            }
        }
        public Boolean isEmpty()
        {
            return empty;
        }
    }
}

 

2.简单的单件类

using System;
namespace SingletonPattern
{
    public class SingltonChocolateBoiler
    {
        //锅炉为空的标志位
        private Boolean empty;
        //指向创建的实例,返回给调用GetInstance()的方法
        public static SingltonChocolateBoiler uniqueChocolateBoiler = null;
        /// <summary>
        /// SingltonChocolateBoiler的私有构造函数
        /// </summary>
        private SingltonChocolateBoiler()
        {
            //开始时,锅炉是空的
            empty = true;
        }
        /// <summary>
        /// 创建instance
        /// </summary>
        /// <returns>SingltonChocolateBoiler instance</returns>
        public static SingltonChocolateBoiler GetInstance()
        {
            if (uniqueChocolateBoiler == null)
            {
                //创建实例
                uniqueChocolateBoiler = new SingltonChocolateBoiler();
                Console.WriteLine(uniqueChocolateBoiler.GetHashCode());
            }
            //返回实例
            return uniqueChocolateBoiler;
        }
        /// <summary>
        /// 如果锅炉为空,用巧克力和牛奶填满锅炉的混合物
        /// </summary>
        public void fill()
        {
            //如果锅炉是空的,则加满锅炉,并将empty标志置为false
            if (isEmpty())
            {
                Console.WriteLine("Fill------------------");
                //将empty标志置为false
                empty = false;
            }
        }
        /// <summary>
        /// 返回锅炉填满状态
        /// </summary>
        /// <returns>empty</returns>
        public Boolean isEmpty()
        {
            return empty;
        }
    }
}

 

3.线程安全的单件类

using System;
namespace SingletonPattern
{
    public class SyncSingletonChocolateBoiler
    {
        private Boolean empty;
        public static SyncSingletonChocolateBoiler uniqueChocolateBoiler = new SyncSingletonChocolateBoiler();
        private SyncSingletonChocolateBoiler()
        {
            Console.WriteLine("empty{}");
            empty = true;
        }
        public static SyncSingletonChocolateBoiler GetInstance()
        {
            Console.WriteLine(uniqueChocolateBoiler.GetHashCode());
            return uniqueChocolateBoiler;
        }
        public void fill()
        {
            if (isEmpty())
            {
                Console.WriteLine("Fill------------------");
                empty = false;
            }
        }
        public Boolean isEmpty()
        {
            return empty;
        }
    }
}

 

4.主程序

using System;
using System.Threading;
namespace SingletonPattern
{
    class Program
    {
        static void Main(string[] args)
        {    
            //1.普通的模式,会创建两个巧克力工厂,Fill方法会调用两次
            ChocolateBoiler chocolateBoiler = new ChocolateBoiler();
            chocolateBoiler.fill();//Fill
            chocolateBoiler = new ChocolateBoiler();
            chocolateBoiler.fill();//Fill
            //2.单件模式,在单线程的代码中,只会创建一个巧克力工厂,Fill方法只会调用一次
            SingltonChocolateBoiler uniqueChocolateBoiler1 = SingltonChocolateBoiler.GetInstance();
            uniqueChocolateBoiler1.fill();
            uniqueChocolateBoiler1 = SingltonChocolateBoiler.GetInstance();
            uniqueChocolateBoiler1.fill();
            //3.单件模式,在多线程的代码中,可能会创建两个巧克力工厂,Fill方法会被调用两次
            Console.WriteLine("SimpleSingleton例子");
            Thread thread1 = new Thread(new ThreadStart(Method1));
            SingltonChocolateBoiler uniqueChocolateBoiler2 = null;
            thread1.Start();
            Thread.Sleep(1);
            for (int i = 0; i < 30; i++)
            {
                Console.WriteLine("Main {0}", i);
                uniqueChocolateBoiler2 = SingltonChocolateBoiler.GetInstance();
                uniqueChocolateBoiler2.fill();
            }
            //4.单件模式,在多线程中,也只会创建一个巧克力工厂,但是由于Fill方法不是线程安全的,所以Fill方法有可能会被调用两次
            Console.WriteLine("StaticInitializeSingleton例子");
            Thread thread2 = new Thread(new ThreadStart(Method2));
            SyncSingletonChocolateBoiler uniqueChocolateBoiler3 = null;
            thread2.Start();
            Thread.Sleep(1);
            for (int i = 0; i < 50; i++)
            {
                Console.WriteLine("Main {0}", i);
                uniqueChocolateBoiler3 = SyncSingletonChocolateBoiler.GetInstance();
                uniqueChocolateBoiler3.fill();
            }
            Console.ReadKey();
        }
        private static void Method1()
        {
            Thread.Sleep(1);
            SingltonChocolateBoiler uniqueChocolateBoiler = null;
            for (int i = 0; i < 30; i++)
            {
                Console.WriteLine("Other {0}", i);
                uniqueChocolateBoiler = SingltonChocolateBoiler.GetInstance();
                uniqueChocolateBoiler.fill();
            }
        }
        private static void Method2()
        {
            Thread.Sleep(1);
            SyncSingletonChocolateBoiler uniqueChocolateBoiler = null;
            for (int i = 0; i < 50; i++)
            {
                Console.WriteLine("Other {0}", i);
                uniqueChocolateBoiler = SyncSingletonChocolateBoiler.GetInstance();
                uniqueChocolateBoiler.fill();
            }
        }
    }
}

 

结果如下:

例子3:单件模式,在多线程的代码中,可能会创建两个巧克力工厂,Fill方法会被调用两次

图片.png

例子4:单件模式,在多线程中,也只会创建一个巧克力工厂,但是由于Fill方法不是线程安全的,所以Fill方法有可能会被调用两次

图片.png

 

回到顶部

十一、本篇所有示例程序下载:

【设计模式】01_Singleton_博客园jackson0714.zip

 

参考资料:

《Head First设计模式》

 




相关文章
|
6月前
|
存储 缓存 Java
9.队列:生产消费模式及线程池的运用
9.队列:生产消费模式及线程池的运用
58 0
|
6月前
|
数据处理
多线程与并发编程【线程对象锁、死锁及解决方案、线程并发协作、生产者与消费者模式】(四)-全面详解(学习总结---从入门到深化)
多线程与并发编程【线程对象锁、死锁及解决方案、线程并发协作、生产者与消费者模式】(四)-全面详解(学习总结---从入门到深化)
67 1
|
3月前
|
NoSQL Redis
Lettuce的特性和内部实现问题之在同步调用模式下,业务线程是如何拿到结果数据的
Lettuce的特性和内部实现问题之在同步调用模式下,业务线程是如何拿到结果数据的
|
3月前
|
NoSQL 关系型数据库 MySQL
简述redis的单线程模式
简述redis的单线程模式
|
4月前
|
Prometheus 监控 数据可视化
通用快照方案问题之Hystrix进行指标监控如何解决
通用快照方案问题之Hystrix进行指标监控如何解决
43 0
|
4月前
|
设计模式 安全 NoSQL
Java面试题:设计一个线程安全的单例模式,并解释其内存占用和垃圾回收机制;使用生产者消费者模式实现一个并发安全的队列;设计一个支持高并发的分布式锁
Java面试题:设计一个线程安全的单例模式,并解释其内存占用和垃圾回收机制;使用生产者消费者模式实现一个并发安全的队列;设计一个支持高并发的分布式锁
65 0
|
4月前
|
存储 设计模式 监控
Java面试题:如何在不牺牲性能的前提下,实现一个线程安全的单例模式?如何在生产者-消费者模式中平衡生产和消费的速度?Java内存模型规定了变量在内存中的存储和线程间的交互规则
Java面试题:如何在不牺牲性能的前提下,实现一个线程安全的单例模式?如何在生产者-消费者模式中平衡生产和消费的速度?Java内存模型规定了变量在内存中的存储和线程间的交互规则
47 0
|
6月前
|
缓存 NoSQL 中间件
【后端面经】【缓存】36|Redis 单线程:为什么 Redis 用单线程而 Memcached 用多线程?epoll、poll和select + Reactor模式
【5月更文挑战第19天】`epoll`、`poll`和`select`是Linux下多路复用IO的三种方式。`select`需要主动调用检查文件描述符,而`epoll`能实现回调,即使不调用`epoll_wait`也能处理就绪事件。`poll`与`select`类似,但支持更多文件描述符。面试时,重点讲解`epoll`的高效性和`Reactor`模式,该模式包括一个分发器和多个处理器,用于处理连接和读写事件。Redis采用单线程模型结合`epoll`的Reactor模式,确保高性能。在Redis 6.0后引入多线程,但基本原理保持不变。
64 2
|
6月前
|
前端开发 网络协议 JavaScript
如何在前端实现WebSocket发送和接收TCP消息(多线程模式)
请确保在你的服务器端实现WebSocket的处理,以便它可以接受和响应前端发送的消息。同时,考虑处理错误情况和关闭连接的情况以提高可靠性。
477 0
|
缓存 编译器 调度
[笔记]Windows核心编程《七》用户模式下的线程同步
[笔记]Windows核心编程《七》用户模式下的线程同步