最近看了一些关于模式实现的文章,发现在实现Singleton模式时,无论是C++,还是Java,或是Object Pascal,无一例外地提到了“双检测锁定”,号称是“既不影响访问速度又解决了并发冲突”,其实这一方法并不可靠。
“双检测锁定”的原理大致是,当试图获得资源时,并不进行锁定操作;如果发现资源分配,则进入互斥区,然后再判断一次,如仍未初始化,再进行初始化操作。由于修改操作在互斥区完成,因此不会有两个线程(或其它运行单位)同时修改,避免了修改的并发冲突。伪代码描述如下:
var
_Instance : instance = nil
function get_instance
if _Instance = nil then
lock;
if _Instance = nil then
_Instance = create_instance
end if
unlock;
end if
Result = _Instance;
end function
粗体字部分为互斥区,同一时刻只能有同一线程进入。而问题在于,虽然同一时刻只能有一个线程进行互斥区,但它并没有保证在某一线程进行互斥区时不允许其它线程进入非互斥区。而在互斥区中如果对_Instance的赋值不是一个原子操作,在非互斥区中对它的访问将不再可靠。
为什么这样说呢?假设我们工作在一个理想的并发环境中,编译器不会对_Instance进行寄存器优化,也不会打乱代码顺序,也没有存储器的cache的同步问题,……,现在我们模拟一下两个线程的运行情况:
线程A 进入get_instance
线程A 取出_Instance的值
线程A 判断_Instance = nil,结果为True
线程A lock;
线程A 取出_Instance的值
线程A 判断_Instance = nil,结果为True
线程A 调用create_instance,成功
线程A 将结果保存到_Instance,第一步成功
线程A 挂起
线程B 进入get_instance
线程B 取出_Instance的值
线程B 判断_Instance = nil,结果为False(很有可能,因为_Instance已经改变)
线程B 返回_Instance
线程B 使用返回值
线程B ……
线程B 挂起
线程A 苏醒
线程A 将结果保存到_Instance,第二步成功
线程A 解锁
线程A ……
现在看一下,线程B调用get_instance得到的结果究竟是什么?不要问我,我实在不知道。关键在于线程A对_Instance进行修改初打断了,而又没有阻止线程B对_Instance进行访问,导致了在一段时间内_Instance处于无法判断的非法状态。如果对_Instance的赋值是一个原子操作则没有这个问题,但很多原因可以使它不是一个原子操作。
那么如何解决这一问题呢?上面已经提到了,导致这一问题出现有两个条件:一是对_Instance的赋值不是原子操作;二是在一个线程进入互斥区时其它线程可以进入到非互区。解决方法自然就是打破这两个条件中的任何一个。
使_Instance的赋值成为原子操作是一个很不错的选择,不过由于_Instance的类型不能随便安排,未必能够实现(不过好象JVM保证了对32位数据的访问是原子操作,不知道这一保证有多大强度),但我们可以换一个思路,就是不去判断_Instance的值,改为判断一个可以用一个原子操作进行赋值的标志(假设可以用Boolean类型):
var
_Instance : instance = nil
_Inited : Boolean = False
function get_instance
if not _Inited then
lock;
if not _Inited then
_Instance = create_instance
call AtomSet(_Inited, True)
end if
unlock;
end if
Result = _Instance;
end function
由于对_Inited标志的赋值是原子操作,它是不会被打断的,所以在前面对_Inited进行判断时,_Inited的值总是有效的。
如果找不到这种类型,还有一种变通方法:
var
_Instance : instance = nil
_Inited : Integer = 0
function get_instance
if _Inited = 0 then //如果这时_Inited不为0,即使是在对它赋值时被打断了,这时_Instance中的值也是完整有效的。
lock;
if _Instance = nil then//由于已经进入了互斥区,不会有其它线程在互斥区中挂起,这一判断是可靠的
_Instance = create_instance
_Inited = 1
end if
unlock;
end if
Result = _Instance;
end function
这样,如果一个线程对_Instance赋值的操作被中断,则_Inited = 0的判断为真,当前线程会去调用lock从而被阻塞;如果一个线程对_Inited的赋值操作被中断,那么这时_Instance中已经是合法的有效值,这时无论_Inited = 0的是否成立都不会有问题了。
上面谈到了使赋值成为原子操作的方法,而另一方法当然就是要使当一个线程处理互斥区中时,其它线程不能访问_Instance。但同时,我们必须允许在没有线程处于互斥区时,所有线程可以同时访问_Instance,如果您对并发控制有一定的经验,马上就可以想到——完全正确,使用multi_read_exclusive_write的锁机制。这样就可以保证当一个线程修改_Instance时,不能有其它线程去读_Instance。但在实现时还要当心死锁,不能这样:
var
_Instance : instance = nil
function get_instance
begin_read
if not _Inited then
begin_write
if not _Inited then
_Instance = create_instance
call AtomSet(_Inited, True)
end if
end_write
end if
Result = _Instance;
end_read
end function
如果这样实现,有可能线程AB都执行了begin_read,然后线程A执行begin_write,被阻塞——等待线程B执行end_read;而线程B也会去执行begin_write,也被阻塞——等待线程A执行end_write……
正确的实现是这样的:
var
_Instance : instance = nil
function get_instance
begin_read
if not _Inited then
end_read
begin_write
if not _Inited then
_Instance = create_instance
call AtomSet(_Inited, True)
end if
end_write
begin_read
end if
Result = _Instance;
end_read
end function
本文地址:http://com.8s8s.com/it/it37502.htm