关于NSCache单例的并发安全性及nonisolated(unsafe)标记的合理性疑问(Swift 6)
嘿,我来帮你把这个问题理清楚~
首先给你吃个定心丸:你的这个单例实现整体是线程安全的,不过nonisolated(unsafe)这个标记其实是多余的,甚至有点画蛇添足,咱们一步步说。
先聊NSCache的线程安全性
你说的没错,NSCache的所有公开API都是官方保证线程安全的,所以你对cache的读写操作,不管在多少个线程里调用,都不会出现数据竞争或者崩溃的情况,这部分你完全不用慌。
再看nonisolated(unsafe)的问题
Swift 6里默认的静态属性(比如你的shared)是自带并发隔离的,但你用nonisolated(unsafe)标记,相当于手动告诉编译器:“我知道这个属性的并发安全我自己兜着,你别管”。但实际上,你的shared是用static let定义的——Swift里static let的初始化本身就是原子性、线程安全的,多线程下只会初始化一次,根本不会有并发问题。所以这里完全没必要加nonisolated(unsafe),反而用了这个标记,之后如果有人修改单例内部逻辑,编译器可能不会再帮你做并发检查,反而埋下隐患。
正确的做法就是直接把这个标记删掉,写成:
public static let shared = CachedNumberFormatters()
就足够安全了。
最后说个小优化点:避免重复创建Formatter
你的cachedFormatter方法有个小竞态条件的问题:比如两个线程同时请求同一个decimalSeparator的Formatter,它们可能同时走到cache.object(forKey:)都返回nil,然后各自创建一个新的Formatter,最后都存入缓存。这不会导致崩溃,但会浪费内存创建重复对象。
如果要解决这个问题,只需要给这个方法加个内部锁就行,比如用NSLock:
public final class CachedNumberFormatters { public static let shared = CachedNumberFormatters() let cache = NSCache<NSString, NumberFormatter>() private let lock = NSLock() // 新增锁 private init() {} public func simpleNumberFormatter(for decimalSeparator: String) -> NumberFormatter { return cachedFormatter(decimalSeparator: decimalSeparator) } func cachedFormatter(decimalSeparator: String) -> NumberFormatter { lock.lock() defer { lock.unlock() } // 确保锁一定会释放 if let cachedObject = cache.object(forKey: decimalSeparator as NSString) { return cachedObject } else { let formatter = NumberFormatter() formatter.decimalSeparator = decimalSeparator cache.setObject(formatter, forKey: decimalSeparator as NSString) return formatter } } }
加锁之后,就能保证同一时间只有一个线程能执行检查缓存和创建对象的逻辑,避免重复创建。
总结一下:
- 你的实现本身线程安全,因为
NSCache和static let单例初始化都自带线程安全保障 - 别用
nonisolated(unsafe),完全没必要,反而会降低编译器的并发检查力度 - 加个锁可以解决重复创建对象的效率问题
备注:内容来源于stack exchange,提问作者ofun

