疑惑:单例模式之双重锁

jingshaohui 2018-07-03 10:59:20
public static Singleton GetInstance()
{
if (uniqueInstance == null)
{
lock (locker)
{
if (uniqueInstance == null)
{
uniqueInstance = new Singleton();
}
}
}
return uniqueInstance;
}

我知道加锁是为了解决线程同步问题,双重锁是为了在解决多线程同时检查加锁状态导致线程阻塞引起的性能问题,外层的null检查本意是为了在uniqueInstance实例非null时无需进行lock部分共享代码的检查,以减少开支。
但是我不明白的是,线程阻塞的位置在哪里?难道不应该是在lock那里吗?如果是在lock那里,线程阻塞时已经进行过null检查了,就算另外一个线程创建过实例了,其他已经和创建实例前一起通过外层null检查的线程依然会进行加锁状态的验证,然后进入锁定的代码块,这难道就是内层null存在的意义?
双重锁的外层null其实是为了解决创建实例后进入要进入共享代码块的线程,而已经一起进入的交给内层null解决,这样理解对吗?我以前没有做过多线程的项目,对线程也不是很熟悉,希望大牛知道一下

...全文
856 21 打赏 收藏 转发到动态 举报
写回复
用AI写文章
21 条回复
切换为时间正序
请发表友善的回复…
发表回复
游北亮 2018-07-05
  • 打赏
  • 举报
回复
楼上说的很不错,
对于1楼说的单例,完全可以用readonly避免,

另外,还有一种依赖注入场景下的单例,简单示例如下, 这场景里是不适合用双重检测的:
public class aaa
{
static readonly Dictionary<Type, object> _instances = new Dictionary<Type, object>();

public T GetSingleInstance<T>()
{
object obj;
var type = typeof(T);
// 这里不能做双重Check,Dictionary并不是线程安全的
// if (!_instances.TryGetValue(type, out obj))
{
lock (_instances)
{
if (!_instances.TryGetValue(type, out obj))
{
obj = ClassLoader.CreateInstance(type);
_instances[type] = obj;
}
}
}
return (T) obj;
}
}
liulilittle 2018-07-04
  • 打赏
  • 举报
回复
这个东西不叫做双重锁,它只是一种优化效率的手段而已 当然这个东西对”业务密集型应用“来说,这么一点点效率毫无意义!

对于单例模式最好的办法就是定义一个 readonly 的静态变量,在静态构造时,就实例单例的目标。
而向上的办法,说的不好听点就是”画蛇舔足“ 在C/C++这类的语言中这种类型的或许没有问题,
但对于类似如”dotNET“、”JVM“这类平台拥有的”公共语言“来说,就是画蛇添足。

而且类似在 dotNET 中,利用单例模式无需要加锁,上面提到了这种写法在dotNET中存属画蛇添足。

第一类:
class Foo1
{
public readonly static Foo1 Current = new Foo1();
}

第二类:
class Foo2
{
private readonly static Foo2 current = new Foo2();

public static Foo2 GetInstance()
{
return current;
}
}

如果你们非要扯效率,那就只能用上述代码的 ”第一类“的方法,而第二类方法的要比第一类低一些,夸大其词的说就是低很多,
第一类只需要仅仅几个汇编指令就获取到”单例化的实例“,而”第二类“需要几十个指令,还要重复的移动内存。当然,这就是
如果你们如果想扯效率来的话!

人们都说 lock 的效率都很低,这些压根没有依据的,人云亦云谁都会,lock 是对 Monitor.Enter 、Monitor.Exit 的一个包装,
Enter 函数开始”竞争锁“时测量当前线程是否已经”Enter“若已获取到”enter flag“它会转换成”偏向锁“仅仅只是,递加
引用计数而已执行这块的代码,超出 1 个 us(微妙)算我输。

若未获取到”enter flag“,它则转入”竞争锁“的过程,但最先只会采取”轻量级锁“挑明白点就是”自旋锁“竞争等待
”enter flag“信号,一段时间后才会转入很笨重的”内核锁“,基本dotNET与JVM采取的测率都差不多,就是算法上不同
而已,要是你不信任,微软搞得 Monitor 自己搞一个类似的来替代,也是可以的。

当然 lock 这个东西如果,用不好效率低下,你不要怪别人,lock 本来就是一把双面人,懂的人用起来非常好用,不懂的人老
整死锁,效率低下,不是只有 lock 的用户代码区域(临界区)只有抛出异常才可能死锁,这个东西里面头头道道多了去了,
引用计数一样会引发死锁。

if(uniqueInstance == null) 这句话是要比调用 Monitor.Enter 快得多,但太微妙,理想时候小于或等于 1 个 us 完成,运气
不好几百个 ns,几个 us 都是小意思。(内核与CPU的负载决定情况,是一个波动的曲线)

但这些对”业务密集型应用“有多大的影响?还这没啥,你能够在一台E3/4的服务器单机一秒处理业务,秒1000,秒5000?
就算给你一台E5你也不一定跑这么搞,影响效率的场景有很多,I/O在其中是最严重的,与之相比锁的消耗,完全就是个笑话!

这篇文章是关于锁的介绍:https://blog.csdn.net/liulilittle/article/details/78938632
正怒月神 2018-07-03
  • 打赏
  • 举报
回复
为了优化效率。
外层的if (uniqueInstance == null) 是考虑到并发时,没有必要再跑到lock中判断了。
  • 打赏
  • 举报
回复
uniqueInstance == null 判断一定要在 lock 内部判断。外层额外判断一次只是为了优化一下。
xuzuning 2018-07-03
  • 打赏
  • 举报
回复
7行的 if (uniqueInstance == null) 只是再次确认没有实例化,以检查 lock 成功之前是否被其他线程实例化过了

3行的 if (uniqueInstance == null) 本是可有可无的
如果说 lock 代价比较大的话,那么 Singleton 对象在多个线程中被共享,任何一个动作都可能产生共享冲突,也就是说每个成员被使用时都应 Lock
仅在 GetInstance() 中节省几次 Lock 是意义不大的
cheng2005 2018-07-03
  • 打赏
  • 举报
回复
外层锁是为了提高性能,减少lock。不为了性能,毫无作用。lock的成本很高
内层锁是核心判断逻辑。
zhangyining128 2018-07-03
  • 打赏
  • 举报
回复
这是双检锁,不是什么双锁。在没有初始化实例时,可能会有多个线程在第一检null时同时判断到实例为null,执行到锁的位置,然后只有一个线程先进入锁定的代码中,执行实例化,而其它剩余的线程阻塞在锁的位置;当那个线程实例化完成后,其它线程才一个接一个的进入锁定的代码中,这时第二检的null判断发现已经实例化,就不用再实例化了。这是最开始调用的一波线程的执行;还有些后续的线程在实例化完成后才开始调用外层的第一检null,这是已经实例化了,因此程序就不会到锁入口的位置了。锁的代价是很高的,即时是依靠cpu的自旋,依然会有约数倍的性能损失。
jingshaohui 2018-07-03
  • 打赏
  • 举报
回复
谢谢各位大佬,关于我提的问题,我明白了,而且还学习到了很多东西,这就结贴了。
wanghui0380 2018-07-03
  • 打赏
  • 举报
回复
引用
不明白你想表达什么,我知道并行和并发的区别,并行靠设备,并发靠编程,但这和我请教的问题有什么关系吗?另外你的最后一句话我实在看不懂什么意思,不好意思,目前水平太低,见识太少,很多东西正在学习。


额,还是一样。XX园告诉你“并行靠设备,并发靠编程”,一样是不知所谓的论调。并行和并发压根不是这么解释

至于和你的问题有什么关系,其实的问题就是在并行编程的情况下,如何处理并发资源。你说有关系么
wanghui0380 2018-07-03
  • 打赏
  • 举报
回复
引用
那按照你这种说法的话双锁不就没什么用了?


那你告诉我“当且仅当”,这4个字何解?,双锁?没有什么双锁,人家只写了一个lock锁,那里来的双锁,这些还是XX园那些人哗众取宠的词,这里没有双锁,这里只是写了2个条件去保证“当且仅当”
jingshaohui 2018-07-03
  • 打赏
  • 举报
回复
引用 13 楼 wanghui0380 的回复:
[quote=引用 10 楼 jingshaohui 的回复:]
[quote=引用 8 楼 hanjun0612 的回复:]
[quote=引用 5 楼 jingshaohui 的回复:]
[quote=引用 4 楼 hanjun0612 的回复:]
为了优化效率。
外层的if (uniqueInstance == null) 是考虑到并发时,没有必要再跑到lock中判断了。

可能我描述的不够清晰,我想问的是,线程并发访问lock代码块所在部分时,阻塞关卡时在lock所在位置,还是在lock所在方法,甚至lock所在对象。个人感觉是从lock所在位置开始进行阻塞,不知道对不对[/quote]
lock (locker) 在这里,会去访问locker这个资源是否锁定了。
所以就在这里阻塞了。[/quote]
学习了,谢谢,也就是说双锁并不会真正把除实例化那个线程外的其他线程完全过滤,只是优化了一部分,优化的效果也是不稳定的,与线程的规模呈负相关,线程越多,同时达到lock哪里的线程越多,耗时越长。[/quote]

什么就性能了,什么就优化了,什么就耗时长了,XX园这种玩意不知所谓。

我们说了,3行是保证返回绝对不为null,7行是保证绝对不会new多个,什么就得出了性能了,优化了,耗时少了,别人保证的是“当且仅当”4个字而已,不是什么性能,什么优化,什么耗时[/quote]
那按照你这种说法的话双锁不就没什么用了?
jingshaohui 2018-07-03
  • 打赏
  • 举报
回复
引用 9 楼 wanghui0380 的回复:
[quote=引用 7 楼 jingshaohui 的回复:]
[quote=引用 2 楼 xuzuning 的回复:]
7行的 if (uniqueInstance == null) 只是再次确认没有实例化,以检查 lock 成功之前是否被其他线程实例化过了

3行的 if (uniqueInstance == null) 本是可有可无的
如果说 lock 代价比较大的话,那么 Singleton 对象在多个线程中被共享,任何一个动作都可能产生共享冲突,也就是说每个成员被使用时都应 Lock
仅在 GetInstance() 中节省几次 Lock 是意义不大的

那这样说的话,是不是可以认为单例模式只能应用于对即时性要求不高的系统,还有只有只读方法的类?[/quote]

哎,XX园流毒甚广,不管什么到XX园那里都是这种忽略前提的论调

而在我们这里,我们说要讨论这个,前提是“你弄清楚,什么是并行,什么是并发了么?”

既然已经是并发了,他就是竞争资源,我们优先解决资源问题,保证稳定性和可控行为。[/quote]
不明白你想表达什么,我知道并行和并发的区别,并行靠设备,并发靠编程,但这和我请教的问题有什么关系吗?另外你的最后一句话我实在看不懂什么意思,不好意思,目前水平太低,见识太少,很多东西正在学习。
cheng2005 2018-07-03
  • 打赏
  • 举报
回复
引用 10 楼 jingshaohui 的回复:
[quote=引用 8 楼 hanjun0612 的回复:]
[quote=引用 5 楼 jingshaohui 的回复:]
[quote=引用 4 楼 hanjun0612 的回复:]
为了优化效率。
外层的if (uniqueInstance == null) 是考虑到并发时,没有必要再跑到lock中判断了。

可能我描述的不够清晰,我想问的是,线程并发访问lock代码块所在部分时,阻塞关卡时在lock所在位置,还是在lock所在方法,甚至lock所在对象。个人感觉是从lock所在位置开始进行阻塞,不知道对不对[/quote]
lock (locker) 在这里,会去访问locker这个资源是否锁定了。
所以就在这里阻塞了。[/quote]
学习了,谢谢,也就是说双锁并不会真正把除实例化那个线程外的其他线程完全过滤,只是优化了一部分,优化的效果也是不稳定的,与线程的规模呈负相关,线程越多,同时达到lock哪里的线程越多,耗时越长。[/quote]
你说的这只是第一次调用,uniqueInstance 为null的情况。
后续更多的调用,都是uniqueInstance 不为null的情况。说来了,这是一个1+ N 次的过程,优化的是N次的这个过程。
wanghui0380 2018-07-03
  • 打赏
  • 举报
回复
引用 10 楼 jingshaohui 的回复:
[quote=引用 8 楼 hanjun0612 的回复:]
[quote=引用 5 楼 jingshaohui 的回复:]
[quote=引用 4 楼 hanjun0612 的回复:]
为了优化效率。
外层的if (uniqueInstance == null) 是考虑到并发时,没有必要再跑到lock中判断了。

可能我描述的不够清晰,我想问的是,线程并发访问lock代码块所在部分时,阻塞关卡时在lock所在位置,还是在lock所在方法,甚至lock所在对象。个人感觉是从lock所在位置开始进行阻塞,不知道对不对[/quote]
lock (locker) 在这里,会去访问locker这个资源是否锁定了。
所以就在这里阻塞了。[/quote]
学习了,谢谢,也就是说双锁并不会真正把除实例化那个线程外的其他线程完全过滤,只是优化了一部分,优化的效果也是不稳定的,与线程的规模呈负相关,线程越多,同时达到lock哪里的线程越多,耗时越长。[/quote]

什么就性能了,什么就优化了,什么就耗时长了,XX园这种玩意不知所谓。

我们说了,3行是保证返回绝对不为null,7行是保证绝对不会new多个,什么就得出了性能了,优化了,耗时少了,别人保证的是“当且仅当”4个字而已,不是什么性能,什么优化,什么耗时
wanghui0380 2018-07-03
  • 打赏
  • 举报
回复
至于lock,lock的是
using()
{这里是lock的范围
}
lock其实就是同步阻塞,阻塞范围就是花括号里面,至于楼上说3行可有可无,其实错误去掉3行,你的retrun在lock外,并发情况,他直接去最后return了,所以加入3行可以保证绝对不会返回null,至于7行则是因为3行不可保证进入6行的唯一行,他可能有多个进入

ps:其实整个过程这个代码是从java里所谓的模式的模式模板,在C#里我们直接用lazy<T>保证,另外写代码始终保证的是目的性,在稳定性和目的性的前提下去优化,如果Y都不确定了,你们的优化,你们的没有前提的“凡是XX,就是YY”的论调有意义么
jingshaohui 2018-07-03
  • 打赏
  • 举报
回复
引用 8 楼 hanjun0612 的回复:
[quote=引用 5 楼 jingshaohui 的回复:]
[quote=引用 4 楼 hanjun0612 的回复:]
为了优化效率。
外层的if (uniqueInstance == null) 是考虑到并发时,没有必要再跑到lock中判断了。

可能我描述的不够清晰,我想问的是,线程并发访问lock代码块所在部分时,阻塞关卡时在lock所在位置,还是在lock所在方法,甚至lock所在对象。个人感觉是从lock所在位置开始进行阻塞,不知道对不对[/quote]
lock (locker) 在这里,会去访问locker这个资源是否锁定了。
所以就在这里阻塞了。[/quote]
学习了,谢谢,也就是说双锁并不会真正把除实例化那个线程外的其他线程完全过滤,只是优化了一部分,优化的效果也是不稳定的,与线程的规模呈负相关,线程越多,同时达到lock哪里的线程越多,耗时越长。
wanghui0380 2018-07-03
  • 打赏
  • 举报
回复
引用 7 楼 jingshaohui 的回复:
[quote=引用 2 楼 xuzuning 的回复:]
7行的 if (uniqueInstance == null) 只是再次确认没有实例化,以检查 lock 成功之前是否被其他线程实例化过了

3行的 if (uniqueInstance == null) 本是可有可无的
如果说 lock 代价比较大的话,那么 Singleton 对象在多个线程中被共享,任何一个动作都可能产生共享冲突,也就是说每个成员被使用时都应 Lock
仅在 GetInstance() 中节省几次 Lock 是意义不大的

那这样说的话,是不是可以认为单例模式只能应用于对即时性要求不高的系统,还有只有只读方法的类?[/quote]

哎,XX园流毒甚广,不管什么到XX园那里都是这种忽略前提的论调

而在我们这里,我们说要讨论这个,前提是“你弄清楚,什么是并行,什么是并发了么?”

既然已经是并发了,他就是竞争资源,我们优先解决资源问题,保证稳定性和可控行为。
正怒月神 2018-07-03
  • 打赏
  • 举报
回复
引用 5 楼 jingshaohui 的回复:
[quote=引用 4 楼 hanjun0612 的回复:]
为了优化效率。
外层的if (uniqueInstance == null) 是考虑到并发时,没有必要再跑到lock中判断了。

可能我描述的不够清晰,我想问的是,线程并发访问lock代码块所在部分时,阻塞关卡时在lock所在位置,还是在lock所在方法,甚至lock所在对象。个人感觉是从lock所在位置开始进行阻塞,不知道对不对[/quote]
lock (locker) 在这里,会去访问locker这个资源是否锁定了。
所以就在这里阻塞了。
jingshaohui 2018-07-03
  • 打赏
  • 举报
回复
引用 2 楼 xuzuning 的回复:
7行的 if (uniqueInstance == null) 只是再次确认没有实例化,以检查 lock 成功之前是否被其他线程实例化过了

3行的 if (uniqueInstance == null) 本是可有可无的
如果说 lock 代价比较大的话,那么 Singleton 对象在多个线程中被共享,任何一个动作都可能产生共享冲突,也就是说每个成员被使用时都应 Lock
仅在 GetInstance() 中节省几次 Lock 是意义不大的

那这样说的话,是不是可以认为单例模式只能应用于对即时性要求不高的系统,还有只有只读方法的类?
  • 打赏
  • 举报
回复
执行外层 if (uniqueInstance == null) 判断时当然是根本还没有 lock 阻塞。

引用 5 楼 jingshaohui 的回复:
个人感觉是从lock所在位置开始进行阻塞,不知道对不对
加载更多回复(1)

110,535

社区成员

发帖
与我相关
我的任务
社区描述
.NET技术 C#
社区管理员
  • C#
  • Web++
  • by_封爱
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告

让您成为最强悍的C#开发者

试试用AI创作助手写篇文章吧