惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
U
Unit 42
GbyAI
GbyAI
M
MIT News - Artificial intelligence
美团技术团队
罗磊的独立博客
雷峰网
雷峰网
量子位
博客园 - 【当耐特】
Last Week in AI
Last Week in AI
D
Docker
小众软件
小众软件
S
SegmentFault 最新的问题
Blog — PlanetScale
Blog — PlanetScale
阮一峰的网络日志
阮一峰的网络日志
宝玉的分享
宝玉的分享
T
Tailwind CSS Blog
WordPress大学
WordPress大学
V
V2EX
博客园_首页
腾讯CDC
The Cloudflare Blog
A
About on SuperTechFans
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC

Kenvix's Blog

实现带有Nvidia GPU+Rootless Podman+Docker+Systemd+自动驱动注入支持的systemd nspawn容器 | Kenvix's Blog 手动保留常用端口,解决 Windows 端口被 Hyper-V / WinNAT 占用的问题 免Telent/TTL屏蔽运营商新版光猫的远控、TR069和RMS,获取动态随机超级管理员密码并固化权限 | Kenvix's Blog 利用Windows卷影副本(Volume Shadow)找回被覆盖和删除的数据 | Kenvix's Blog 在Windows下实现WireGuard动态DNS解析(DDNS)的正确方法:避免无意义的开销 | Kenvix's Blog OpenWRT/DNSMasq 配置DHCP静态路由主动推送 实现流量直达和旁路由流量零代价分载 | Kenvix's Blog 解决 Windows 打开视频/图片文件夹很慢的问题 | Kenvix's Blog AltA2DP - 向支持Sony LDAC协议的耳机提供Windows下蓝牙LDAC音频编码器支持 | Kenvix's Blog (2024更新)修复黑群晖 DSM7.0 + Btrfs 存储空间/磁盘损毁/堪用 的问题 校园网白嫖思路分享:局域网中转-不花钱、不认证、高速上网 | Kenvix's Blog 在 Windows 上配置网卡多个 VLAN、多个虚拟网卡、实现单线多拨网速叠加(无需驱动支持) | Kenvix's Blog 解决视频彩铃、语音通话自动转视频通话导致打电话自动挂断的问题 | Kenvix's Blog 超低成本廉价考研教程:如何用小于¥500甚至¥300的开销考个研 | Kenvix's Blog 在 Ubuntu 21.10 上启用蓝牙 LDAC/AAC/AptX 高质量音频编码支持 在 VMware Workstation 桥接模式的网卡上让虚拟机使用 VLAN 的正确方法 在 Windows 上设置 NAT 或网络共享的正确方法——避免Wi-Fi热点无法使用 自编译 红米 AC2100 OpenWRT R21.7.26 Linux 内核结构和子系统简介 | Kenvix's Blog Java 快速读取文本 (算法竞赛适用) | Kenvix's Blog 状态压缩的动态规划问题:骨牌完全覆盖棋盘问题 | Kenvix's Blog 我的 Windows 10 2004 新增 Bug 解决办法记录 解决 Android Studio 及 IDEA 中 Gradle 错误信息乱码的问题 Kotlin 的那些骚操作 | Kenvix's Blog 在普通的 Gradle Java/Kotlin 项目中使用 BuildConfig 修复国行 MIUI 打开 Google Play 始终提示 DF-DFERH-01 的问题 解决 VSCode 持续调用 WMIC 导致一个 CPU 核心完全被占满的问题 扔鸡蛋问题 | Kenvix's Blog 计算机幻觉从入门到入土 | Kenvix's Blog Java 注解预处理 Annotation Processing & 代码生成 Ubuntu 上通过以太网分享网络连接(NAT) | Kenvix's Blog
利用 ThreadLocal + Lambda,实现有状态变量的单例模式 | Ken...
2020-08-19 · via Kenvix's Blog

通常情况下单例模式的对象应该具有状态,然而现实是复杂的,总会有那么一些特殊情况下需要小小地【违例】一下。

动机

一个父类的方法执行前需要设置一个变量的值,变量值会对方法的执行结果产生影响。现希望子类以单例的方式继承父类。

以我实际遇到的一个问题为例,JOOQ 是一个 ORM 类库,这个类库能够自动扫描数据库并生成 DAO,但是自动生成的 DAO 功能有限,通常需要继承来扩展这些 DAO。每个 DAO 在实例化时需要传入一个 Configuration,这个 Configuration 包含有关数据库的信息。

通常情况下 Configuration 可以由所有的 DAO 共享,然而,在启动事务后 Configuration 会被 JOOQ 派生,且后续所有的事务内数据库操作都应该使用派生的 Configuration

现希望子 DAO 为单例,同时,在不改变父 DAO 的情况下,且不修改子 DAO 的已有方法和对这些方法的调用的情况下,实现 configuration 变量的 “智能” 修改。

我们可以利用 ThreadLocal + Lambda 解决这个问题。

基本概念

ThreadLocal

ThreadLocal<E> 是一个容器,它内部采用 Map 实现,以线程的某种唯一特征为键,用户自定义类型 E 为值。因此不同线程存取同一个 ThreadLocal 容器会得到不同的值,且不同线程互不影响。一个线程总是只能访问到属于他自己的那份值。

Kotlin 协程

建议先跳过本节

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 //还原初始状态
    }
}

var currentThreadJooqConfiguration: Configuration
    get() = currentThreadJooqConfigurationOrNull ?: jooqConfiguration
    set(value) { currentThreadJooqConfigurationOrNull = value }

//合理利用 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>()

val jooqConfiguration: Configuration
    get() = realJooqConfiguration.derive() // derive() 等效 clone(),直接取的总是派生出来的。

private lateinit var realJooqConfiguration: Configuration // 派生的根基。只做派生用途。

由于使用了 ThreadLocal,不同线程从该容器取出的结果各不相同且不会互相影响。同时,由于我们的代码霸占着这个线程,因此这里虽然临时改变了 jooqConfigurationOrNull ,但是对其他线程并没有影响。

针对一开始提出的问题,要让父 DAO 获得的 Configuration 也发生改变,只需重写父类的 getter,让父类总是从 ThreadLocal 容器获得值即可。

局限

不难发现,刚才的做法实际上是有漏洞的。

  1. 如果 lambda 代码块内调用线程池执行其他代码,绝对不能执行和数据库操作相关的动作。这是容易理解的,我们所修改的 Configuration 只在当前线程上下文起作用,调用线程池实际上就脱离了当前上下文。
  2. 不应该在 lambda 代码块内直接调用挂起函数。即使所调用的挂起函数并没有操作数据库也不可以。这是因为当前线程在空闲时可能会被安排执行其他协程任务,导致隐患。另外,当协程跑到其他线程上执行时上下文会丢失。
  3. (Kotlin) 不应该在 lambda 代码块内进行 return。若导致 use 函数没有执行后续还原初始状态的代码会导致大问题。

漏洞 1 可以通过人为规范避免。Kotlin 在语法上避免了 3 问题。

针对问题 2,要执行 suspend 函数比较困难,但是也不是不能做。首先,通过 runBlocking 使得当前线程处于阻塞状态,不允许安排其他协程任务(但遗憾的是你的函数也不肯在当前线程跑)。然后,使用 asContextElement 使协程上下文携带某种信息。但这意味着前面的代码几乎都得为此重写,总体上还是比较难做的。