How and what you lock on depends upon what you're doing.
Let's say that you're working with a device of some kind - say a coffee maker. You might have a class that looks like this:
public CoffeeMaker {
private IntPtr _coffeeHandle;
private Object _lock = new Object();
}
In this case, you are protecting access to the _coffeeHandle - a pointer/handle to a real physical device, so this is pretty easy:
public int AvailableCups {
get {
lock (_lock) {
return GetAvailableCups(_coffeeHandle); // P/Invoked
}
}
}
public void Dispense(int nCups)
{
lock (_lock) {
int nAvail = GetAvailableCups(_coffeeHandle);
if (nAvail < nCups) throw new CoffeeException("not enough coffee.");
Dispense(_coffeeHandle, nCups); // P/Invoked
}
}
So if I'm running a multithreaded app, I (probably) don't want to read the number of cups that are available while I'm dispensing (maybe it's a hardware error). By protecting accesses to the handle, I can ensure that. Also, I can't be asked to dispense while I'm already dispensing - that would be bad, so that's protected too. Finally, I don't dispense unless I have enough coffee available and you notice that I don't use my public property to check that - this way the action of ensuring there's enough coffee and dispensing are tied together. The magic word is atomic - they can't be cut apart without creating issues.
You use a static object as a lock if you have one and only one instance of a resource that needs protecting. Think, "do I have a singleton?" and that will be a guideline for when you might need a static lock. For example, let's say that CoffeeMaker has a private constructor. Instead, you have a factory method that constructs coffee machines:
static Object _factLock = new Object();
private CoffeeMaker(IntPtr handle) { _coffeeHandle = handle; }
public static CoffeeMaker GetCoffeeMaker()
{
lock (_factLock) {
IntPtr _handle = GetCoffeeMakerHandle(); // P/Invoked
if (_handle == IntPtr.Zero) return null;
return new CoffeeMaker(_handle);
}
}
Now in this case, it feels like CoffeeMaker should implement IDisposable so that handle gets taken care of, because if you don't release it then somebody might not be getting their coffee.
There are a few problems though - maybe if there's not enough coffee, we should make more - and that takes a long time. Heck - dispensing coffee takes a long time, which is why we're careful to protect our resources. Now you're thinking that really all this coffee maker stuff should be in a thread of its own and that there should be an event that gets fired when the coffee is done, and then it starts to get complicated and you understand the importance of knowing what you're locking on and when so that you don't block making coffee because you asked how many cups are there.
And if the words "deadlock", "atomic", "monitor", "wait", and "pulse" all sound foreign to you, you should consider reading up on multiprocessing/multithreading in general and see if you can solve the fair barbershop problem or the dining philosophers problem, both quintessential examples of resource contention.