剑指offer之面试题2:实现Singleton模式

2019-09-04 17:14:03 浏览数 (1)

来源:剑指offer

这篇主要记录《剑指offer》书籍中的面试题2:实现Singleton模式

使用语言:C#

代码环境:VS2017

总共有5中解法,从前往后依次优化。

结构如下:

前言

这里先给出调用程序的代码

Program.cs

代码语言:javascript复制
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World!");

            //Task.Run(() =>
            //{
            //    Console.WriteLine(Singleton1.Instance);
            //});
            //Task.Run(() =>
            //{
            //    Console.WriteLine(Singleton1.Instance);
            //});


            //Task.Run(() =>
            //{
            //    Console.WriteLine(Singleton2.Instance);
            //});
            //Task.Run(() =>
            //{
            //    Console.WriteLine(Singleton2.Instance);
            //});

            //Task.Run(() =>
            //{
            //    Console.WriteLine(Singleton3.Instance);
            //});
            //Task.Run(() =>
            //{
            //    Console.WriteLine(Singleton3.Instance);
            //});

            //Task.Run(() =>
            //{
            //    Console.WriteLine(Singleton4.Instance);
            //});
            //Task.Run(() =>
            //{
            //    Console.WriteLine(Singleton4.Instance);
            //});
           

            Task.Run(() =>
            {
                Console.WriteLine(Singleton5.Instance);
            });
            Task.Run(() =>
            {
                Console.WriteLine(Singleton5.Instance);
            });
            Console.ReadKey();

        }
    }

这里,会在每次创建一种Singleton模式的实现方法之后,在这里调用。

里面会用两个线程来模拟多线程的情况。

而在单例的实现中,会在创建构造函数时,输出语句,来区别是否创建了多个对象。

效果如下示例:

构造函数只调用了一次。

方法一

单线程情况下的一般实现。

代码如下:

Singleton1.cs

代码语言:javascript复制
 1     public sealed class Singleton1
 2     {
 3         //私有的构造函数
 4         private Singleton1()
 5         {
 6             //Console.WriteLine($"Singleton1生成了...{Guid.NewGuid()}");
 7         }
 8         private static Singleton1 instance = null;
 9         /// <summary>
10         /// 在静态属性Instance中,只有在instance为null时,才创建一个实例以避免重复
11         /// </summary>
12         public static Singleton1 Instance
13         {
14             get
15             {
16                 //如果instance为空,则创建实例
17                 if (instance == null)
18                 {
19                     //Thread.Sleep(3000);  //模拟多线程同时到达这里
20                     instance = new Singleton1();
21                 }
22                 return instance;
23             }
24         }
25     }

在上述代码中,Singleton1的静态属性Instance中,只有在instance为null的时候才创建一个实例以避免重复创建。

同时我们把构造函数定义为私有函数,这个就可以确保只创建一个实例。

但是这种方法只适合单线程,多线程情况下,就有问题了。

方法二

为了保证多线程环境下,我们还是只能得到一个类型的实例,需要加上一个同步锁。

代码如下:

Singleton2.cs

代码语言:javascript复制
 1     public sealed class Singleton2
 2     {
 3         /// <summary>
 4         /// 私有的构造函数
 5         /// </summary>
 6         private Singleton2()
 7         {
 8             Console.WriteLine($"Singleton2生成了...{Guid.NewGuid()}");
 9         }
10         //用作同步锁
11         private static readonly object syncObj = new object();
12 
13         private static Singleton2 instance = null;
14 
15 
16         public static Singleton2 Instance
17         {
18             get
19             {
20                 //添加同步锁
21                 lock (syncObj)
22                 {
23                     //如果instance为null,则新建
24                     if (instance == null)
25                     {
26                         Thread.Sleep(3000);
27                         instance = new Singleton2();
28                     }
29                       
30                 }
31                 return instance;
32             }
33         }
34     }

我们还是假设有两个线程同时想创建一个实例。由于在一个时刻只有一个线程能得到同步锁,

当第一个线程加上锁时,第二个线程只能等待。当第一个线程创建出实例之后,第二个线程就不会重复创建实例了,

这样就保证了我们在多线程环境中也只能得到一个实例。

但是呢,这种方法也不完美。我们每次通过属性Instance得到Singleton2的实例,都会试图加上一个同步锁,

而加锁时一个非常耗时的操作,在没有必要的时候我们应该尽量避免。

方法三

我们可以这样:加同步锁前后两次判断实例是否已经存在。

我们只是在实例还没有创建之前需要加锁操作,以保证只有一个线程创建出实例。而当实例已经创建之后,我们已经不需要再做加锁操作了。

改进代码如下:

Singleton3.cs

代码语言:javascript复制
 1     public sealed class Singleton3
 2     {
 3         //私有构造函数
 4         private Singleton3()
 5         {
 6             Console.WriteLine($"Singleton3生成了...{Guid.NewGuid()}");
 7         }
 8         //创建同步锁对象
 9         private static readonly object syncObj = new object();
10 
11         private static Singleton3 instance = null;
12 
13         public static Singleton3 Instance
14         {
15             get
16             {
17                 //instance为null,这里可能有两个线程同时到达,即都判断为null的情况
18                 if (instance == null)
19                 {
20                     Thread.Sleep(3000); //会同时有两个线程等在这里
21                     //加上同步锁,即只放一个线程过去
22                     lock (syncObj)
23                     {
24                         //如果此时instance还未null,则新建instance;否则,跳过
25                         if(instance==null)
26                             instance = new Singleton3();
27                     }
28                 }
29                 return instance;
30             }
31         }
32     }

这样的代码实现起来比较复杂,容易出错,我们还有更优秀的解法。

方法四

C# 语法中有一个函数能够保证只调用一次,那就是静态构造函数。

代码如下:

Singleton4.cs

代码语言:javascript复制
 1      public sealed class Singleton4
 2     {
 3         static Singleton4() //静态构造函数
 4         {
 5             Console.WriteLine($"Singleton4生成了...{Guid.NewGuid()}");
 6         }
 7 
 8         private static Singleton4 instance = new Singleton4();
 9         
10         public static Singleton4 Instance
11         {
12             get
13             {
14                 Thread.Sleep(3000);
15                 return instance;
16             }
17         }
18     }

由于C# 是在调用静态构造函数时初始化静态变量,.NET运行时能够确保只调用一次静态构造函数,这样我们就能够保证只初始化一次instance。

C#中调用静态构造函数的时机不是由程序员掌控的,而是当.NET运行时,发现第一次使用一个类型的时候自动调用该类型的静态构造函数。

方法五

这种方法实现的Singleton,可以很好的解决 方法四 中Singleton4的实例创建时机过早的问题:

代码语言:javascript复制
 1     public sealed class Singleton5
 2     {
 3         Singleton5()
 4         {
 5             Console.WriteLine($"Singleton5生成了...{Guid.NewGuid()}");
 6         }
 7 
 8         public static Singleton5 Instance
 9         {
10             get
11             {
12                 return Nested.instance;
13             }
14         }
15 
16         class Nested
17         {
18             static Nested()
19             {
20 
21             }
22             internal static readonly Singleton5 instance = new Singleton5();
23         }
24     }

在这段代码中,我们在内部定义了一个私有类型的 Nested。

当我们第一次用到这个嵌套类型的时候,会调用静态构造函数创建Singleton5的实例 instance。

类型Nested只在属性Singleton5.Instance中被用到,由于其私有属性,他人无法使用Nested类型。

因此,当我们第一次试图通过属性Singleton5.Instance得到Singleton5的实例时,会自动调用Nested的静态构造函数创建实例 instance。

总结

推荐解法,方法四,或者方法五

其中方法四利用了C#的静态构造函数的特性,确保只创建一个实例。

第五种方法利用了私有嵌套类型的特性,做到只在需要的时候才会创建实例,提高空间使用率。

0 人点赞