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

推荐订阅源

量子位
小众软件
小众软件
S
SegmentFault 最新的问题
人人都是产品经理
人人都是产品经理
博客园 - 【当耐特】
博客园 - 三生石上(FineUI控件)
C
Check Point Blog
S
Schneier on Security
Microsoft Azure Blog
Microsoft Azure Blog
N
Netflix TechBlog - Medium
Engineering at Meta
Engineering at Meta
GbyAI
GbyAI
罗磊的独立博客
有赞技术团队
有赞技术团队
V
V2EX
Y
Y Combinator Blog
博客园 - 叶小钗
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
F
Fortinet All Blogs
W
WeLiveSecurity
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Stack Overflow Blog
Stack Overflow Blog
The Cloudflare Blog
S
Security @ Cisco Blogs
TaoSecurity Blog
TaoSecurity Blog
MyScale Blog
MyScale Blog
Hugging Face - Blog
Hugging Face - Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
www.infosecurity-magazine.com
www.infosecurity-magazine.com
PCI Perspectives
PCI Perspectives
H
Heimdal Security Blog
Schneier on Security
Schneier on Security
Security Latest
Security Latest
AWS News Blog
AWS News Blog
月光博客
月光博客
Security Archives - TechRepublic
Security Archives - TechRepublic
Recent Announcements
Recent Announcements
Google DeepMind News
Google DeepMind News
博客园 - Franky
Cisco Talos Blog
Cisco Talos Blog
T
Threat Research - Cisco Blogs
M
MIT News - Artificial intelligence
T
Troy Hunt's Blog
N
News and Events Feed by Topic
Cloudbric
Cloudbric
Scott Helme
Scott Helme
云风的 BLOG
云风的 BLOG
Attack and Defense Labs
Attack and Defense Labs

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 Windows 选择指定的网卡来开承载网络型热点 | Kenvix's Blog 修复升级 Windows10 版本后所有内置应用闪退+第三方应用参数错误的问题 | Kenvix's Blog 配置用于 Gradle6.x + MySQL 8 的 jOOQ 3.14 代码自动生成 (已更新) 修复 Windows 环境下的程序访问 WSL 中的 MySQL 提示 Access Denied 的问题 修复 WSL 下 PHP+FastCGI 卡死的问题 使用任意磁盘或路径保存 Windows 文件历史记录 | Kenvix's Blog [1.12.2+Mod] MoeCraft :: 自由开放的科技向公益 Mod 服务器 Kenvix's Blog 禁用使用Intel核显的Windows笔记本自动调节亮度功能 | Kenvix's Blog 真正实现Minecraft高级登录(外置登录)的几种方案 | Kenvix's Blog 谈谈神舟的两艘贼船,Z7M-KP7S1 / Z7M-KP7SC USBCopyer: 插上U盘自动按需复制文件 | Kenvix's Blog USBCopyer 回调功能详细说明 | Kenvix's Blog C# 实现自定义"应用程序设置"的配置文件(user.config)存储路径 | Kenvix's Blog Win10 资源管理器为所有格式激活“编辑”按钮并修改文本文件“编辑”按钮的编辑器 | Kenvix's Blog 留言板 | Kenvix's Blog 又一次 Hello world | Kenvix's Blog Java 学习笔记 (仍在更新) | Kenvix's Blog 在Win10 Pro下挂载NFS(网络文件系统) | Kenvix's Blog Nginx 反向代理 Aria2 JSONRPC | Kenvix's Blog (Android6.0~9.0) 清除锁屏密码 | Kenvix's Blog WordPress 更换站点地址后批量修改文章/评论中的旧地址 | Kenvix's Blog 修复一加3/3T因固件过老导致刷入ROM时提示错误7的问题 | Kenvix's Blog 修复Android DM-Verity 警告 | Kenvix's Blog 贴吧云签到 资源索引(下载|文档|插件) | Kenvix's Blog 继续监控!使用树莓派+Motion实现实时视频监控并通过浏览器查看 | Kenvix's Blog 自动获取Pixiv每日排行榜第一张图片(600x600 | 可用于博客背景图) | Kenvix's Blog 使用树莓派实现定时拍照监控并发送邮件到邮箱 | Kenvix's Blog 好压 V2.7 Beta1 绿色版——功能强大,良心的压缩软件 | Kenvix's Blog 任意语言实现读取压缩包注释 | Kenvix's Blog 自己实现QQ群自定义分享(管理员开启了群交易?) | Kenvix's Blog MoeCDN - 加速Gravatar/GoogleAPIs等无法在国内访问的资源 | Kenvix's Blog [Minecraft] WebLogin-连接到你的服务器来检查玩家是否可以登录 | Kenvix's Blog Android卡刷包提示This package is for device: ... this device is ...的解决方案 Kenvix's Blog 给EMLOG评论框加上复选框[√]防止垃圾评论 | Kenvix's Blog 欢迎使用emlog | Kenvix's Blog
利用 ThreadLocal + Lambda,实现有状态变量的单例模式 | Kenvix's Blog
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 使协程上下文携带某种信息。但这意味着前面的代码几乎都得为此重写,总体上还是比较难做的。