В .NET есть ConcurrentDictionary. У него есть GetOrAdd. Метод сам по себе безопасен для || доступа в смысле инвариантов словаря. Но он не гарантирует, что передаваемый в него делегат порождения объекта будет вызван ровно один раз. Это неприятно, если словарь у нас - это кеш каких-то «тяжёлый» в плане создания конструкций. Лайф-хак тут хранить не сами объекты, а Lazy-обёртки. Типа ConcurrentDictionary> вместо ConcurrentDictionary.
Развёрнутый рассказ можно почитать, например, тут:
https://andrewlock.net/making-getoradd-on-concurrentdictionary-thread-safe-using-lazy/.