First, your assumption about the exception thrown is incorrect:
NullException (the _instance member is Null).
The only NullReferenceException that would be thrown from your GetInstance() method is the one you throw yourself. Assuming you have no code anywhere else that resets the _instance value to null, the only place that method dereferences the _instance value is at a statement that cannot be reached without ensuring that _instance is initialized to some non-null value.
As for the broader problem, IMHO the biggest issue is that you have a poorly-defined problem, and a broken implementation.
Even ignoring the question of whether the "singleton" (it's not really a singleton, but for the sake of argument let's call it that for now) is ever changed, the initialization is not thread safe. You have the following potential race (assuming a single CPU core to make the illustration simple):
Thread 1 Thread 2
-------- --------
call GetInstance()
if (_instance == null)
--> preempted <--
call GetInstance()
if (_instance == null)
...
_instance = new SingletonClass(id);
...
return _instance;
--> preempted <--
if (_instance == null)
...
_instance = new SingletonClass(id);
...
return _instance;
As you can see in the above example, the way the code is written now, each thread could independently try to retrieve the instance, would see the current value as null, and would create a new instance object to be returned.
For a true singleton, the best way to implement this is with the Lazy<T> class:
private static readonly Lazy<SingletonClass> _instance =
new Lazy<SingletonClass>(() => new SingletonClass());
public static SingletonClass Instance { get { return _instance.Value; } }
In this case, the Lazy<T> class handles all of the initialization work, including ensuring it's done in a thread-safe way.
In your case, where you do not have a true singleton, the above won't work. The Lazy<T> pattern only works when something is to be initialized just once, but you want to be able to change it on the fly. Given that, you need something more like this:
private static ISingletonClass _instance = null;
private static readonly object _lock = new object();
public static ISingletonClass GetInstance(string id = null)
{
lock (_object)
{
if (_instance == null || (id != null && _instance.Id != id))
{
if (id == null)
{
throw new ArgumentNullException("id");
}
_instance = new SingletonClass(id);
}
return _instance;
}
}
The above will ensure against threads initializing the field concurrently. They can race only to the lock, and then one thread is guaranteed to be the only one to initialize the object, assuming each thread passes the same value for id.
That said, that only fixes the basic thread-safety issue in your code. There are some bigger problems.
First, what do you expect the code to do if and when one thread retrieves the current instance, then some other thread changes the current instance before the first thread is done using the instance it retrieved? I'm not saying this is inherently broken, but it is at the very least very fragile, and you absolutely need to think about this scenario and decide for yourself what the right behavior should be in that case.
Second, this is a really fragile mutation of the singleton pattern. A true singleton will have a single object, allocated only once during the lifetime of the process. This ensures simplicity of design and predictability of behavior.
The implementation you have, will necessarily make it a lot harder to understand what the code is doing at any given point. It is practically guaranteed to add large amounts of time to some developer's day, either yours or someone else's, when some bug comes up and they are trying to track down the actual cause of the bug.
The fact that the instances of this class are tied to a string ID suggests that a better approach might be to maintain a Dictionary<string, SingletonClass> objects, requiring all callers to always specify the ID, and using that ID to retrieve (possibly initializing lazily of course) the object that thread needs at that moment.
I strongly recommend a different design. But at a minimum, if you decide you must go this direction, make sure that you have considered all of the various combinations of thread events, and not only decided what the correct behavior is in each given scenario, but added code to ensure any assumed constraints.
NullReferenceExceptionis a reserved exception and thus you shouldn't throw it.ArgumentNullException