Swift 单例中的线程安全问题

单例是常见的一种设计模式。最近在编写单例代码的时候,发现公司很多同事的 Swift 单例写法都是这样的,

extension NSObject {
    @discardableResult
    static func kep_synchronized<T>(_ lock: AnyObject, closure: () -> T) -> T {
        objc_sync_enter(lock)
        defer { objc_sync_exit(lock) }

        return closure()
    }
}

class DefaultDict: NSObject {
    private static var manager: DefaultDict?
    static var sharedManager: DefaultDict {
        get {
            var newShared = manager
            kep_synchronized(self) {
                if newShared == nil {
                    newShared = DefaultDict()
                    manager = newShared
                }
            }
            return newShared!
        }
    }
}

先是对 NSObject 进行了拓展,加了个锁方法(应该是互斥锁)。随后在单例类里面加了两个属性,一个是实际上的单例属性(初始是nil),另一个是只提供 get 方法的只读属性,并且在这个只读属性里面加上了锁。

一开始我以为这个锁是为了线程安全加上的,但后来研究了一下发现这个锁其实是没有必要的。

下面是实验代码(注意不要在 Playground 里面试验,直接新建工程,把代码写在 viewDidAppear里面):

extension NSObject {
    @discardableResult
    static func kep_synchronized<T>(_ lock: AnyObject, closure: () -> T) -> T {
        objc_sync_enter(lock)
        defer { objc_sync_exit(lock) }

        return closure()
    }
}

class ViewController: UIViewController {

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view.
    }

    override func viewDidAppear(_ animated: Bool) {
        super.viewDidAppear(animated)
        
        let count = 10000

        for index in 0..<count {
            DefaultDict.sharedManager.set(value: index, key: String(index))
        }

        DispatchQueue.concurrentPerform(iterations: count) { index in
            print("value will read with index \(String(index))")
            if let n = DefaultDict.sharedManager.object(key: String(index)) as? Int {
                print("value read with index \(String(n))")
            }
        }

        DefaultDict.sharedManager.reset()

        DispatchQueue.concurrentPerform(iterations: count) { index in
            print("value will set with index \(String(index))")
            DefaultDict.sharedManager.set(value: index,key: String(index))
            print("value set with index \(String(index))")
        }
    }
}

对类 DefaultDict 的写法分为以下几种:

  1. 直接声明一个静态常量属性存储单例(static let),对单例属性 dict 的处理不放在串行队列里面
class DefaultDict {
    private var dict:[String: Any] = [:]
    public static let sharedManager = DefaultDict()
    private init() {
        
    }
    
    public func set(value: Any, key: String) {
        dict[key] = value
    }
    
    public func object(key: String) -> Any? {
        dict[key]
    }
    
    public func reset() {
        print("reset")
        dict.removeAll()
    }
}
  1. 使用我司常见的单例加锁初始化方式,但同样不把对属性 dict 的处理放串行队列里面
class DefaultDict: NSObject {
    private var dict:[String: Any] = [:]
    private static var manager: DefaultDict?
    static var sharedManager: DefaultDict {
        get {
            var newShared = manager
            kep_synchronized(self) {
                if newShared == nil {
                    newShared = DefaultDict()
                    manager = newShared
                }
            }
            return newShared!
        }
    }
    
    override init() {
        super.init()
    }
    
    public func set(value: Any, key: String) {
        dict[key] = value
    }
    
    public func object(key: String) -> Any? {
        dict[key]
    }
    
    public func reset() {
        print("reset")
        dict.removeAll()
    }
}
  1. 使用我司常见的单例加锁初始化方式,但把对属性 dict 的处理放互斥锁里面
class DefaultDict: NSObject {
    private var dict:[String: Any] = [:]
    private static var manager: DefaultDict?
    static var sharedManager: DefaultDict {
        get {
            var newShared = manager
            kep_synchronized(self) {
                if newShared == nil {
                    newShared = DefaultDict()
                    manager = newShared
                }
            }
            return newShared!
        }
    }
    
    override init() {
        super.init()
    }
    
    public func set(value: Any, key: String) {
        Self.kep_synchronized(self) {
            self.dict[key] = value
        }
    }
    
    public func object(key: String) -> Any? {
        var result: Any?
        Self.kep_synchronized(self) {
            result = self.dict[key]
        }
        return result
    }
    
    public func reset() {
        print("reset")
        Self.kep_synchronized(self) {
            self.dict.removeAll()
        }
    }
}
  1. 直接声明一个静态常量属性存储单例(static let),把对单例属性 dict 的处理放在串行队列里面
class DefaultDict: NSObject {
    private var dict:[String: Any] = [:]
    
    private let serialQueue = DispatchQueue(label: "serialQueue")
    
    static var sharedManager =  DefaultDict()
    
    override init() {
        super.init()
    }
    
    public func set(value: Any, key: String) {
        serialQueue.sync {
            self.dict[key] = value
        }
    }
    
    public func object(key: String) -> Any? {
        var result: Any?
        serialQueue.sync {
            result = self.dict[key]
        }
        return result
    }
    
    public func reset() {
        print("reset")
        serialQueue.sync {
            self.dict.removeAll()
        }
    }
}

上面几种不同写法的运行结果是:

  1. 互斥锁和串行队列都不用。崩溃
  2. 用互斥锁,单例写法为我司的 get 和 optional 结合的方式
    1. 只在获取单例全局属性的地方,也就是 sharedManager 加上互斥锁。会出现崩溃
    2. 在改变属性值的地方也加上互斥锁。不会崩溃
  3. 用串行队列同步任务,单例写法为直接赋值。不会崩溃
  4. 单例获取的地方用互斥锁(get 和 optional 结合),改变值的地方用串行队列。不会崩溃

所以,在获取单例静态变量的地方加锁并不能保证他是线程安全的,只能保证多个线程获取到的单例静态变量是同一个。但是这种写法应该是多余的。Swift 中 let 声明的变量都是线程安全的,所以直接用static let sharedManager = DefaultDict()这种方式声明单例就可以了。

如果是为了懒加载,而使用我司那种 get 和 optional 结合的方式声明单例的话,那更没必要了。因为 Swift 中的全局常量和变量都是懒加载的

Global constants and variables are always computed lazily, in a similar manner to Lazy Stored Properties. Unlike lazy stored properties, global constants and variables don’t need to be marked with the lazy modifier.

想要保证对某个属性值的操作是线程安全的,就只能对这些操作加锁或者放到串行队列同步任务里面