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.

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

Read more

《漫步华尔街(第12版)》读书笔记

《漫步华尔街(第12版)》读书笔记

股票分析 基本面分析 * 基本面分析的四个基本决定因素 * 预期增长率 * 复合增长(复利)对投资决策有很重要的意义。 * 一只股票的股利增长和盈利增长率越高,理性投资者应愿意为其支付越高的价格。 * 推论:一只股票的超常增长率持续时间越长,理性投资者应愿意为其支付越高的价格。 * 预期股利支付率 * 对于预期增长率相同的两只股票来说,持有股利支付率越高的股票,较之股利支付率低的股票,会使你的财务状况更好。 * 在其他条件相同的情况下,一家公司发放的现金股利占其盈利的比例越高,理性投资者应愿意为其股票支付越高的价格。 * 特例,很多处于强劲增长阶段的公司,往往不支付任何股利。这时候不满足「在其他条件相同的情况下」。 * 风险程度 * 在其他条件相同的情况下,一家公司的股票风险越低,理性投资者(以及厌恶风险的投资者)应愿意为其股票支付越高的价格。 * 市场利率水平 * 在其他条件相同的情况下,市场利率越低,理性投资者应愿意为股票支付越高的价格。 * 举例,银行存款利率

By Gray
2025 端午日本九日游

2025 端午日本九日游

从日本回来后就一直忙个不停,忙着搬家和工作。这周末终于有时间回顾和记录一下日本的旅游行程。 这次出国游是年初就规划好的。端午节假期三天再加上节后请假四天,以及周末,总共能休 9 天。5 月 31 号出发,6 月 9 号凌晨的航班飞回北京。 出发前的准备 机票和酒店 越临近出发日期,机票和酒店就越贵。所以我们早早地就把机票和酒店定了。 去程机票订的山航,青岛转机,5 月 31 号从北京出发抵达青岛,在青岛玩一天,翌日早上从青岛飞往关西机场。回程机票订的海南航空,从东京羽田机场直飞北京,是凌晨两三点的红眼航班。 本次行程要去关西(京都、大阪、奈良)、关东(东京、富士山)。关西三个城市很近,一直住在京都即可,从京都往返大阪和奈良。关东就住在东京。京都的酒店订在了京都站附近,出站走几步就能到,交通非常便利。东京的酒店订在了马喰町附近,附近有很多地铁线路,包括浅草线、

By Gray
2025 关税危机中学到的投资经验

2025 关税危机中学到的投资经验

充足的现金流很重要 好的买入机会不会每天都出现,但当它出现的时候,你最好还有筹码可以投入。 有些人手里握不住钱,一有闲钱就赶紧买入基金、股票,生怕错过了机会,让钱白搭手里。市场是疯狂的、充满变数的,尤其是在特朗普上台后,一句话就可能让股市涨停或跌停。那些专业的理财投资机构尚不能预测市场,何况我们这些散户呢。在不稳定的市场中,我们要学习巴菲特,备好现金,耐心等待买入(抄底)机会。 不要提前打光子弹 美股标普 500 指数从 2 月中旬到 3 月中旬累计跌了约 10%。如果这时候你觉得已经跌了很多,可以 all in 抄底了,那么你就会错过 4 月上旬的那次狂跌——一周跌了约 10%。没有人能预测市场,除了此刻的股市指挥家特朗普。散户们能学到的经验就是「永远不要提前打光子弹」,你以为的谷底其实只是个半山腰。 相信自己,保持耐心 在美股大跌的时段里,小红书、v2ex

By Gray