In a memory allocator, don't you generally want to maintain freelists of memory that is available for reuse? If so, isn't there a race between threads trying to unmap the freed blocks and threads trying to reuse them?
The design goal of the allocator I worked on (http://www.scott-a-s.com/projects/#streamflow) specifically, and scalable allocators in general, is for threads to avoid interfering with each other. You're probably thinking of one giant, global free list that all threads interact with. That's not what happens. Instead, each thread has its own set of free lists. When thread A calls malloc, you grant those requests from thread A's data structures. Threads B and C cannot allocate memory form these spaces.
Cribbing from my own description of the approach:
"Streamflow avoids synchronization across threads as much as possible, and only uses lock-free synchronization when it is unavoidable. The common allocation path avoids synchronization by using per-thread private heaps, and a lock-free remote free list. If a thread needs to allocate memory, and it has sufficient free memory to satisfy the request, it only needs to touch data structures that it owns. If a thread frees memory that it allocated, it also only needs to touch data structures that it owns. The tricky case is when a thread needs to free memory that a different thread allocated. That memory gets added to the allocating thread’s remote free list in a lock-free manner—but the allocating thread will only touch that list when it does not have sufficient free memory."
So, if a thread does not have enough free chunks to grant a request, it first checks its remote-free lists. (That is, memory freed by other threads, even though it was allocated by this thread.) This means that a thread checking its remote-free list will be relatively rare, meaning that remote frees are unlikely to encounter contention. If it still can't grant the request, then it makes a request for an entirely new free list from a global page manager. That global page manager, in my implementation, was protected by a lock. Some recent follow up work eliminated this global lock (I think, see http://aces.snu.ac.kr/SFMalloc.html).
We can also avoid this problem because scalable memory allocators try to minimize different threads touching the same parts of the internal data structure. This is counter to, say, trying to implement a lock-free red black tree. There, the whole point is that multiple threads will all touch the same state.
The allocator I worked on maintained lock-free queues for remote frees. We would sometimes deallocate these queues entirely - but we knew this was safe because we would only do it when all of the chunks of memory attached to these queues were free. If there was a dangling pointer somewhere, that was an application error.
Data structures inside memory allocators serve a different purpose than data structures inside typical applications. When you have, say, a tree in your application, you very much care about the actual data stored in it. Your payload - the entire point of having the data structure - is the particular data stored somewhere in the tree. If you want, say, the element with the key "ABCD", then no other element will do. The payload for the data structures in a memory allocator is just chunks of memory. If you're doing an allocation, then any chunk will do. If you're doing a free, you need to free that particular chunk, but you can also assume that no one else will free it.