In the Singleton design pattern there should be one and only one copy of a given object. That's the contract. And while ColdFusion lacks the tools to fulfill this pattern in its entirely, we worked hard in our previous article to ensure that only one valid working copy of a singleton existed.
But we still have a problem.
Our defense against duplication was done partly in the initialization code of the singleton object, which returned a reference to an existing object if it existed, leaving the copy in an uninitialized state. The other half of our defensive strategy was based on the following function:
<cffunction name="getSingleton" returntype="any"> <cfif not structKeyExists('application','_singleton')> <cfset application._singleton= createObject("component","singleton").init()> </cfif> <cfreturn application._singleton> </cffunction>
Basically it's a caching factory method, which returns a reference to an existing singleton if it exists, or creates a new one otherwise. Unfortunately, there's still one scenario under which one or more copies may appear, known in computing parlance as a race condition.
In a multi-threaded environment like a web server, user requests can occur and be processed simultaneously. But what happens when two requests occur, but slightly out out of step?
- Process A calls getSingleton() for the first time.
- Process B calls getSingleton().
- Process A checks to see if _singleton exists in the application scope. It doesn't.
- Process B also checks to see if _singleton exists in the application scope. It doesn't.
- Process A makes a new singleton.
- Process B makes a new singleton.
- Process A assigns the result and exits, returning application._singleton.
- Process A assigns its copy of singleton and also exits, returning its application._singleton.
And hey-presto! Each of those requests has its own singleton, breaking the design contract.
How to we fix it? With a lock, of course. But with a twist.
<cffunction name="getSingleton" returntype="any"> <cfif not structKeyExists('application','_singleton')> <cflock name="_singleton" type="exclusive" timeout="60"> <cfif not structKeyExists('application','_singleton')> <cfset application._singleton =createObject("component","singleton").init()> </cfif> </cflock> </cfif> <cfreturn application._singleton> </cffunction>
Note that we've wrapped the instantiation code in an exclusive lock, and inside that lock we do a double check, testing again that the object hasn't been created. Why?
Because otherwise we could end up in the same scenario: both see _singleton doesn't exist, both wait for the lock, and then both create their own instance. With the double-check, however, the second process that enters the lock will see that the object already exists. It will then safely exit the lock, falling through to return the now existing singleton.
Always keep in mind that your code is operating in a multi-threaded environment and program defensively. While the actual probability that a problem like the above will occur is rare, it can happen, and trying to debug a problem or crash that only appears once in a blue moon is a nasty, nasty experience.
Race condidtion can require locking but not allways.
With the above code the race condition isn't actally an issue, as the newly created instance is assigned to application._singleton and as only one instance of that can exist the worse that can happen is that two objects are created but the same instance is returned from both calls.
Steps 7 and 8 would return the exactly the same instance.
Posted by: Justin Mclean | April 12, 2007 at 08:19 AM
Justin: It depends. If process B completed step 6 and then blocked, process A then has a chance to exit with its value on step 7, then B has a chance to do the same on step 8 whenever it unblocks.
That's the problem with multi-threading, you never know if the processes will run simultaneously, concurrently, or in fits and starts as threads block and unblock and the system responds to interupts, garbage collections, and so on.
Best to synchronize things so any potential bugs are zapped before they can grow into more serious problems.
Posted by: Michael Long | April 12, 2007 at 08:54 AM
Even if the above occurs the instances will still be the same as the code returns a reference to the application instance rather than a copy of it.
But yes it's probably best to put the locking in and avoid this sort of issue from potentially occuring.
Posted by: Justin Mclean | April 12, 2007 at 09:54 AM