Monitor
takes an object reference, an object has a special header the CLI uses to store information about the lock, and how it promotes the lock to a kernel event if needed.
Object header
The most significant bytes in a typical object header format is shown below.
|31 0|
----------------|
|7|6|5|4|3|2| --|
| | | | | |
| | | | | +- BIT_SBLK_IS_HASHCODE : set if the rest of the word is a hash code (or sync block index)
| | | | +--- BIT_SBLK_IS_HASH_OR_SYNCBLKINDEX : set if hashcode or sync block index is set
| | | +----- BIT_SBLK_SPIN_LOCK : lock the header for exclusive mutation on spin
| | +------- BIT_SBLK_GC_RESERVE : set if the object is pinned
| +--------- BIT_SBLK_FINALIZER_RUN : set if finalized already
+----------- BIT_SBLK_AGILE_IN_PROGRESS : set if locking on AppDomain
Once you create a lock / Monitor on an object, the CLR will look at the header and first determine whether it needs to find any locking information in the Sync Block table, it does this simply by looking at the bits that are set. If there is no Thin Lock, it will create one (if applicable). If there is a Thin Lock it will try to spin and wait for it. If the header has been inflated, it will look in the Sync Block Table for locking information.
A Thin Lock basically consists of an App Domain Index, Recursion Level, and a Managed Thread Id. The Thread Id is atomically set by a locking thread if zero, or if nonzero, a simple spin wait is used to reread the lock state various times to acquire the lock. If after a period of time the lock is still not available, it will need to promote the lock (and if not already done so), inflate the Thin Lock to the Sync Block Table and a true* lock will need to be registered with the operating based on a Kernel Event (like an auto reset event).
Note this was an excerpt from a larger slightly related answer of mine, if you are extremely bored take a look here
Why Do Locks Require Instances In C#?