通常情况下单例模式的对象 不 应该具有状态,然而现实是复杂的,总会有那么一些特殊情况下需要小小地【违例】一下。
一个父类的方法执行前需要设置一个变量的值,变量值会对方法的执行结果产生影响。现希望子类以单例的方式继承父类。
以我实际遇到的一个问题为例,JOOQ 是一个 ORM 类库,这个类库能够自动扫描数据库并生成 DAO,但是自动生成的 DAO 功能有限,通常需要继承来扩展这些 DAO。每个 DAO 在实例化时需要传入一个 Configuration
,这个 Configuration
包含有关数据库的信息。
通常情况下 Configuration
可以由所有的 DAO 共享,然而,在启动事务后 Configuration
会被 JOOQ 派生,且后续所有的事务内数据库操作都应该使用派生的 Configuration
。
现希望子 DAO 为单例,同时,在不改变父 DAO 的情况下,且不修改子 DAO 的已有方法和对这些方法的调用的情况下,实现 configuration 变量的 “智能” 修改。
我们可以利用 ThreadLocal + Lambda 解决这个问题。
ThreadLocal<E>
是一个容器,它内部采用 Map 实现,以线程的某种唯一特征为键,用户自定义类型 E
为值。因此不同线程存取同一个 ThreadLocal 容器会得到不同的值,且不同线程互不影响。一个线程总是只能访问到属于他自己的那份值。
Kotlin 协程实际上是由 Kotlin 管理的一个线程池。
协程的 挂起 指的是当前正在运行一块协程代码的线程从这块代码脱离,不再负责这块代码的执行。线程暂时处于空闲状态。每当执行到一个 suspend 函数调用时,都会发生挂起。
协程 挂起
发生后,开始执行 suspend 函数,负责具体执行这个函数的线程由函数的 withContext()
调用决定。 注意
:此时发生了线程的切换,脱离了原先的线程。
suspend 函数完成后,刚才挂起的协程代码 恢复 执行。 注意 :此时再次发生线程切换,刚才没执行的代码继续回到原先的线程执行。
要解决一开始提出的问题,容易想到,我们可以利用 ThreadLocal 声明一个“全局”变量,当 DAO 需要用到 Configuration
时,就从 ThreadLocal 容器去取。
同时,再声明一个函数,负责临时更改 ThreadLocal
容器的 Configuration
具体值,当传入的 lambda 执行完毕后再改回原先状态。
传入的 lambda,就是我们希望在新的 Configuration
上下文中所执行的代码。
fun <R> Configuration.use(then: ((Configuration) -> R)): R { //jooqConfigurationOrNull 是负责管理存储 Configuration 的属性 val initialState = jooqConfigurationOrNull //保存初始状态 jooqConfigurationOrNull = this //临时变更为新状态 return try { //注意捕获异常,防止发生异常时无法还原状态 then(this) //执行传入的 lambda 代码块 } catch (exception: Throwable) { logger.debug("Config use block failed: $exception") throw exception //原样抛出异常 } finally { logger.debug("Recover thread local jooq config to initial") jooqConfigurationOrNull = initialState //还原初始状态 } } //合理利用 getter 和 setter 让 ThreadLocal 对用户不可见 var jooqConfigurationOrNull: Configuration? get() = currentThreadJooqConfigurationContainer.get() set(value) { if (value == null) currentThreadJooqConfigurationContainer.remove() else currentThreadJooqConfigurationContainer.set(value) } // 真正的全局 ThreadLocal 容器 private val currentThreadJooqConfigurationContainer = ThreadLocal<Configuration>()
由于使用了 ThreadLocal
,不同线程从该容器取出的结果各不相同且不会互相影响。同时,由于我们的代码霸占着这个线程,因此这里虽然临时改变了 jooqConfigurationOrNull
,但是对其他线程并没有影响。
针对一开始提出的问题,要让父 DAO 获得的 Configuration
也发生改变,只需重写父类的 getter,让父类总是从 ThreadLocal 容器获得值即可。
不难发现,刚才的做法实际上是有漏洞的。
Configuration
漏洞 1
可以通过人为规范避免。Kotlin 在语法上避免了 2
3
问题。
针对问题 2
,若要执行 suspend 函数。可以通过 runBlocking
强制协程在当前线程上继续执行任务,也就是使得当前线程处于阻塞状态,不允许安排其他协程任务。