在属性的willSet与didSet中使用NSLock保护多线程写入是否可行?
我想了解在属性的willSet和didSet中使用NSLock来保护多线程环境下的写入操作是否为可行的解决方案。示例代码如下:
private let lock = NSLock() private static var cachedUserTokenDetails: UserTokenDetails? { willSet { lock.lock() } didSet { lock.unlock() } } class func getUserTokenDetails() -> UserTokenDetails? { if cachedUserTokenDetails == nil { cachedUserTokenDetails = getUserTokenDetailsFromKeychain() } return cachedUserTokenDetails }我认为这种方式是安全的,因为willSet和didSet总会被调用,请问是否正确?
咱们直接说结论:你这个实现并不安全,问题出在几个关键细节上——willSet/didSet确实总会执行,但你的锁用法本身就存在核心问题:
1. 锁的作用域完全不匹配
你定义的lock是实例属性,但cachedUserTokenDetails是静态类属性。这意味着你的类每创建一个实例,就会生成一个独立的lock,而静态缓存属性是全局共享的。多线程环境下,不同线程可能通过不同实例的lock尝试保护同一个静态属性,这相当于没有锁——多个线程能同时进入赋值操作,完全起不到线程同步的作用。
2. 读取操作完全没加锁
你的getUserTokenDetails方法里,读取cachedUserTokenDetails判断是否为nil的过程没有任何锁保护。多线程场景下,一个线程正在执行赋值操作(持有某个lock),另一个线程可能同时在读取这个属性,这会导致读取到不完整的中间状态(比如属性内存正在被写入但还没完成),这也是典型的线程不安全场景。
3. 锁的覆盖范围不对
即使锁的作用域没问题,你把lock放在willSet和didSet里,也只能覆盖赋值前后的瞬间,而整个赋值操作(包括属性的内存写入)是在willSet和didSet之间执行的。但更关键的是,读取操作完全在锁的保护之外,这根本无法保证多线程下的读写安全。
正确的实现方式
要解决这些问题,我们需要做到两点:让锁和静态属性同作用域,并且所有读写操作都在锁的保护下。这里给你两种可行的写法:
写法一:直接在方法里加锁(更简洁)
private static let lock = NSLock() private static var _cachedUserTokenDetails: UserTokenDetails? // 私有存储属性 class func getUserTokenDetails() -> UserTokenDetails? { lock.lock() defer { lock.unlock() } // 用defer确保锁一定会释放,哪怕后面代码抛出异常 if _cachedUserTokenDetails == nil { _cachedUserTokenDetails = getUserTokenDetailsFromKeychain() } return _cachedUserTokenDetails }
写法二:用计算属性封装锁(更符合属性封装的习惯)
private static let lock = NSLock() private static var _cachedUserTokenDetails: UserTokenDetails? private static var cachedUserTokenDetails: UserTokenDetails? { get { lock.lock() defer { lock.unlock() } return _cachedUserTokenDetails } set { lock.lock() defer { lock.unlock() } _cachedUserTokenDetails = newValue } } class func getUserTokenDetails() -> UserTokenDetails? { if cachedUserTokenDetails == nil { cachedUserTokenDetails = getUserTokenDetailsFromKeychain() } return cachedUserTokenDetails }
这两种写法都能保证所有对缓存属性的读写操作都在同一个静态lock的保护下,真正实现多线程安全。
内容的提问来源于stack exchange,提问作者ricardopereira

