4

单例模式使用饿汉式和懒汉式创建一定安全?很多人不知 - realyrare

 2 years ago
source link: https://www.cnblogs.com/mhg215/p/16548218.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

单例模式大概是23种设计模式里面用的最多,也用的最普遍的了,也是很多很多人一问设计模式都有哪些必答的第一种了;我们先复习一下饿汉式和懒汉式的单例模式,再谈其创建方式会带来什么问题,并一一解决!还是老规矩,先上代码,不上代码,纸上谈兵咱把握不住。

饿汉式代码

    public class SingleHungry
    {
        private readonly static SingleHungry _singleHungry = new SingleHungry();
        private SingleHungry()
        {
        }
        public static SingleHungry GetSingleHungry()
        {
            return _singleHungry;
        }
    }

代码很简单,意思也很明确,接着我们写点代码测试验证一下;

第一种测试: 构造函数私有的,new的时候报错,因为我们的构造函数是私有的。

 SingleHungry  _singleHungry=new SingleHungry();
第二种测试: 比对创建多个对象,然后多个对象的Hashvalue
public class SingleHungryTest
    {
        public static void FactTestHashCodeIsSame()
        {
            Console.WriteLine("单例模式.饿汉式测试!");
            var single1 = SingleHungry.GetSingleHungry();
            var single2 = SingleHungry.GetSingleHungry();
            var single3 = SingleHungry.GetSingleHungry();
            Console.WriteLine(single1.GetHashCode());
            Console.WriteLine(single2.GetHashCode());
            Console.WriteLine(single3.GetHashCode());
        }
    }
测试下来,三个对象的hash值是一样的。如下图:
1046844-20220803161551708-1324606779.png

饿汉式结论总结

饿汉式的单例模式不推荐使用,因为还没调用,对象就已经创建,造成资源的浪费;

懒汉式代码

    public class SingleLayMan
    {
        //1、私有化构造函数
        private SingleLayMan()
        {

        }
        //2、声明静态字段  存储我们唯一的对象实例
        private static SingleLayMan _singleLayMan;
        //通过方法 创建实例并返回
        public static SingleLayMan GetSingleLayMan1()
        {
            //这种方式不可用  会创建多个对象,谨记
            return _singleLayMan = new SingleLayMan();
        }
        /// <summary>
        ///懒汉式单例模式只有在调用方法时才会去创建,不会造成资源的浪费
        /// </summary>
        /// <returns></returns>
        public static SingleLayMan GetSingleLayMan2()
        {
            if (_singleLayMan == null)
            {
                Console.WriteLine("我被创建了一次!");
                _singleLayMan = new SingleLayMan();
            }
            return _singleLayMan;
        }
    }
 public class SingleLayManTest
    {
        /// <summary>
        /// 会创建多个对象.hash值不一样
        /// </summary>
        public static void FactTest()
        {
            Console.WriteLine("单例模式.懒汉式测试!");
            var singleLayMan1 = SingleLayMan.GetSingleLayMan1();
            var singleLayMan2 = SingleLayMan.GetSingleLayMan1();
            Console.WriteLine(singleLayMan1.GetHashCode());
            Console.WriteLine(singleLayMan2.GetHashCode());
        }
        /// <summary>
        /// 单例模式.懒汉式测试:懒汉式单例模式只有在调用方法时才会去创建,不会造成资源的浪费,但会有线程安全问题
        /// </summary>
        public static void FactTest1()
        {
            Console.WriteLine("单例模式.懒汉式测试!");
            var singleLayMan1 = SingleLayMan.GetSingleLayMan2();
            var singleLayMan2 = SingleLayMan.GetSingleLayMan2();
            Console.WriteLine(singleLayMan1.GetHashCode());
            Console.WriteLine(singleLayMan2.GetHashCode());
        }
        /// <summary>
        /// 单例模式.懒汉式多线程环境测试!
        /// </summary>
        public static void FactTest2()
        {
            Console.WriteLine("单例模式.懒汉式多线程环境测试!");
            for (int i = 0; i < 10; i++)
            {
                new Thread(() =>
                {
                    SingleLayMan.GetSingleLayMan2();
                }).Start();
            }

            //Parallel.For(0, 10, d => {
            //    SingleLayMan.GetSingleLayMan2();
            //});
        }
    }

懒汉式结论总结

懒汉式的代码如上已经概述,上面GetSingleLayMan1()会创建多个对象,这个没什么好说的,肯定不推荐使用;GetSingleLayMan2()是大多数人经常使用的,可解决刚才因为饿汉式创建带来的缺点,但也带来了多线程的问题,如果不考虑多线程,那是够用了。


话说回来,既然刚才饿汉式和懒汉式各有其优缺点,那我们该如何抉择呢?到底选择哪一种?

其它方式创建单例—饿汉式+静态内部类

    public class SingleHungry2
    {
        public static SingleHungry2 GetSingleHungry()
        {
            return InnerClass._singleHungry;
        }       
        public static class InnerClass
        {
            public readonly static SingleHungry2 _singleHungry = new SingleHungry2();
        }
    }

这个代码,用了饿汉式结合静态内部类来创建单例,线程也安全,不失为创建单例的一种办法。

其它方式创建单例—懒汉式+反射

 首先我们解决一下刚才懒汉式创建单例的线程安全问题,上代码:

 /// <summary>
    /// 通过反射破坏创建对象
    /// </summary>
    public class SingleLayMan1
    { 
        //私有化构造函数
        private SingleLayMan1()
        {
        }
        //2、声明静态字段  存储我们唯一的对象实例
        private static SingleLayMan1? _singleLayMan;
        private static object _oj = new object();

/// <summary> /// //解决多线程安全问题,双重锁定,减少系统消耗,节约资源 /// </summary> public static SingleLayMan1 GetSingleLayMan() { if (_singleLayMan == null) { lock (_oj) { if (_singleLayMan == null) { _singleLayMan = new SingleLayMan1(); Console.WriteLine("我被创建了一次!"); } } } return _singleLayMan; } }

具体描述,在代码里面已经说得足够清楚,一看肯定明白,我们还是写点测试代码,验证一下,上代码:

public class SingleLayManTest1
    {
        public static void FactTestReflection()
        {
            var singleLayMan1= SingleLayMan1.GetSingleLayMan();

            var type = Type.GetType("_01单例模式.反射破坏单例模式.SingleLayMan1");
            //获取私有的构造函数
            var ctors = type?.GetConstructors(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
            //执行构造函数
            SingleLayMan1 singleLayMan = (SingleLayMan1)ctors[0].Invoke(null);
            Console.WriteLine(singleLayMan1.GetHashCode());
            Console.WriteLine(singleLayMan.GetHashCode());
        }
    }

上面的代码分别通过SingleLayMan1.GetSingleLayMan2()和反射创建对象,输出二者对象hash值比较,结果肯定是不一样的,重点是我们可以通过反射创建对象。

通过上面的代码,不知道大家有没有意识到我们虽通过加锁解决了线程安全问题,但仍会出现问题;正常创建对象的顺序是:

1、new 在内存中开辟空间
2、 执行构造函数 创建对象
3、 把空间指向我们的对像

但如果因为我们的程序使用多线程,则会发生"指令重排",本应执行顺序为1、2、3,实际执行顺序为1、3、2,但这种情况很少,不过我们写程序嘛,肯定追求严谨一点准没错。

如果需要解决该问题需要给定义的私有局部变量加关键字 加上volatile (意思不稳定的 ,可变的) ,加该关键字可以避免指令重排。具体代码主要是这句如下:

 private volatile static SingleLayMan? _singleLayMan;

到这里,大家认为还有没有问题?答案是肯定的,不然我就不会写这篇文章了,通过反射既然可以创建对象,那么我们写的创建实例代码还有什么意义,有没有什么办法避免反射创建对象呢?

如果认真看了之前的反射创建对象代码,肯定发现反射是通过构造函数来创建对象的,那么我们相应的就在构造函数处理一下。来,我们继续上代码:

 /// <summary>
    /// 解决反射创建对象的问题
    /// </summary>
    public class SingleLayMan3
    {
        //2、声明静态字段  存储我们唯一的对象实例
        private volatile static SingleLayMan3? _singleLayMan;
        private static object _oj = new object();
        //私有化构造函数
        private SingleLayMan3()
        {
            lock (_oj)
            {
                if (_singleLayMan != null)
                {
                    throw new Exception("不要通过反射来创建对像!");
                }
            }
        }

        /// <summary>
        /// //解决多线程安全问题,双重锁定,减少系统消耗,节约资源
        /// </summary>
        public static SingleLayMan3 GetSingleLayMan()
        {
            if (_singleLayMan == null)
            {
                lock (_oj)
                {
                    if (_singleLayMan == null)
                    {
                        _singleLayMan = new SingleLayMan3();
                        Console.WriteLine("我被创建了一次!");
                    }
                }
            }           
            return _singleLayMan;
        }
       
    }

下面继续上测试代码,验证一下:

public class SingleLayManTest3
    {
        /// <summary>
        /// 第一次通过调用 SingleLayMan3.GetSingleLayMan()创建对象导致_singleLayMan不为空,之后再去通过反射创建对象时,构造函数里面判断创建对象导致_singleLayMan变量,报异常
        /// </summary>
        public static void FactTestReflection()
        {
            var singleLayMan1= SingleLayMan3.GetSingleLayMan();

            var type = Type.GetType("_01单例模式.反射破坏单例模式.SingleLayMan3");
            //获取私有的构造函数
            var ctors = type?.GetConstructors(System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic);
            //执行构造函数
            SingleLayMan3 singleLayMan = (SingleLayMan3)ctors[0].Invoke(null);
            Console.WriteLine(singleLayMan1.GetHashCode());
            Console.WriteLine(singleLayMan.GetHashCode());
        }
    }

结论其实测试方法已经说明:第一次通过调用 SingleLayMan3.GetSingleLayMan()创建对象导致_singleLayMan不为空,之后再去通过反射创建对象时,构造函数里面判断创建对象导致_singleLayMan变量,报异常。

其实到这里,有人肯定发现了问题,第一次通过去执行自己写的创建单例方法来创建对象,后面再执行反射时才会报异常,那有没有什么办法,只要有人第一次反射创建对象时就报异常呢?

定义局部变量解决反射创建对象问题

 public class SingleLayMan4
    {
        //2、声明静态字段  存储我们唯一的对象实例
        private volatile static SingleLayMan4? _singleLayMan;
        private static object _oj = new object();
        private static bool _isOk = false;
        //私有化构造函数
        private SingleLayMan4()
        {
            lock (_oj)
            {
                if (_isOk == false)
                {
                    _isOk = true;
                }
                else
                {
                    throw new Exception("不要通过反射来创建对像!只有第一次通过反射创建对象会成功!请做第一个吃葡萄的人!");
                }
            }
        }

        /// <summary>
        /// //解决多线程安全问题,双重锁定,减少系统消耗,节约资源
        /// </summary>
        public static SingleLayMan4 GetSingleLayMan()
        {
            if (_singleLayMan == null)
            {
                lock (_oj)
                {
                    if (_singleLayMan == null)
                    {
                        _singleLayMan = new SingleLayMan4();
                        Console.WriteLine("我被创建了一次!");
                    }
                }
            }           
            return _singleLayMan;
        }
       
    }

测试代码,验证一下:

public static void FactTestReflection()
        {
            //第一次创建对象会成功
            var singleLayMan1 = GetReflectionSingleLayMan4Instance();

            //第二次创建对象会失败,报异常
           var singleLayMan2 = GetReflectionSingleLayMan4Instance();

            Console.WriteLine(singleLayMan1.GetHashCode());
        }
        private static SingleLayMan4 GetReflectionSingleLayMan4Instance()
        {
            var type = Type.GetType("_01单例模式.反射破坏单例模式.SingleLayMan4");
            //获取私有的构造函数
            var ctors = type?.GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic);
            //执行构造函数
            SingleLayMan4 singleLayMan = (SingleLayMan4)ctors[0].Invoke(null);
            return singleLayMan;
        }

第一次创建对象会成功,因为执行构造函数时没有执行GetSingleLayMan(),跨过了new,导致_isOk赋值true,第二次反射创建执行构造函数时判断变量_isOk为true,走入异常逻辑。

但这样做真的就安全了吗?既然可以通过反射执行构造函数来创建对象,那也可以通过反射改变局部变量_isOk 的值,上代码:

        /// <summary>
        /// 通过反射也可以改变局部变量_isOk的值,继续创建对象
        /// </summary>
        public static void FactTestReflection2()
        {
            Type type = Type.GetType("_01单例模式.反射破坏单例模式.SingleLayMan4");
            //获取私有的构造函数
            var ctors = type?.GetConstructors(BindingFlags.Instance | BindingFlags.NonPublic);
            //执行构造函数
            SingleLayMan4 singleLayMan1 = (SingleLayMan4)ctors[0].Invoke(null);
            FieldInfo fieldInfo =  type.GetField("_isOk", BindingFlags.NonPublic | BindingFlags.Static);
            fieldInfo.SetValue("_isOk", false);
            SingleLayMan4 singleLayMan2 = (SingleLayMan4)ctors[0].Invoke(null);

            Console.WriteLine(singleLayMan1.GetHashCode());
            Console.WriteLine(singleLayMan2.GetHashCode());
        }

大家或许发现了,只要有反射存在,哪怕你的逻辑写的再严谨,它仍然可以反射创建对象,只因为它是反射!所以,单例模式的安全性也是相对而言的,具体选择用哪个,取决项目的业务场景了。如有发现问题,欢迎不吝赐教!

源码地址:https://gitee.com/mhg/design-mode-demo.git


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK