The general case here is whether or not a synchronization object is re-entrant. In other words, can be acquired again by the same thread if it already owns the lock. Another way to say that is whether the object has "thread affinity".
In .NET, the Monitor class (which implements the lock statement), Mutex and ReaderWriterLock are re-entrant. The Semaphore and SemaphoreSlim classes are not, you could get your code to deadlock with a binary semaphore. The cheapest way to implement locking is with Interlocked.CompareExchange(), it would also not be re-entrant.
There is an extra cost associated with making a sync object re-entrant, it needs to keep track of which thread owns it and how often the lock was acquired on the owning thread. Which requires storing Thread.ManagedId and a counter, two ints. This affected choices in C++ for example, the C++11 language specification finally adding threading to the standard library. The std::mutex class is not re-entrant in that language and proposals to add a recursive version were rejected. They considered the overhead of making it re-entrant too high. A bit heavy-handed perhaps, a cost that's rather miniscule against the amount of time spent on debugging accidental deadlock :) But it is a language where it is no slamdunk that acquiring the thread ID can be guaranteed to be cheap like it is in .NET.
This is exposed in the ReaderWriterLockSlim class, you get to choose. Note the RecursionPolicy property, allowing you to choose between NoRecursion and SupportsRecursion. The NoRecursion mode is cheaper and makes it truly slim.