前言

前段时间,我们处理了一则Java线程池配置不当导致的线上问题(参见 好端端的线程池,怎么就卡死了?),本文将以此为案例,使用形式化语言,从数学角度进行证明。

形式化证明简介

首先需要搞清楚一个概念,形式化证明,也是通过编程的形式进行的,只不过这段代码使用形式化编程语言进行表达,从数理层面来看更为严谨,常见的形式化语言有:

  • Coq:广泛用于学术研究和软件形式化验证,历史悠久。
  • Lean4:最近在数学界引起了广泛关注,适合开发严格的数学证明。
  • Dafny:与C#和Java有相似的语法,适合编程语言内置规范。
  • ACSL:基于C语言的注释,适合进行语法和语义检查。
  • TLA+:用于建模算法和程序的形式语言,特别适合并发和分布式系统。

某些领域也有特定的工具,比如 Java Pathfinder (JPF) 用于Java程序的形式化验证,支持线程和并发的验证。

鉴于Lean4使用最为广泛,本次首先使用Lean4进行尝试。

Lean4 的安装

Lean4 可以在 vscode 中安装使用,也需要安装 Mathlib 数学库(类似于我们常用的三方依赖库)。本节将介绍其安装和使用方法。

Lean4 也有网页版 https://live.lean-lang.org/,但实测网站响应速度较差,推荐本地安装使用。

  1. 安装 vscode,我们使用这个 IDE 创建和运行 Lean 工程。
  2. 安装 Lean4 插件:打开 vscode 中的插件市场,搜索 Lean4,点击安装。

  1. 安装 elan:它是Lean4版本管理工具,类似于node里面的nvm。到官方网站 https://github.com/leanprover/elan?tab=readme-ov-file#elan-lean-version-manager 参考即可:

  1. 配置 mathlib:创建项目时使用 lean 的项目构建工具,指定 mathlib4,会自动安装 mathlib 依赖包:
lake +leanprover-community/mathlib4:lean-toolchain new <your_project_name> math

到这里,就可以开始编写 Lean 代码了(vscode会启动lean4服务器,允许即可)。

常用功能:

形式化证明

前面我们遇到的问题是:当任务数量大于线程池的核心线程数量后,由于任务间的依赖关系导致线程无法释放,最终导致新提交的任务被放到无界阻塞队列中,无法得到处理,最终系统处于假死状态。

为验证线程池资源是否存在资源耗尽的漏洞,可以参考如下Prompt(比较粗糙,作为示例已经够用了),让AI编写一段 Lean4 形式化代码:

你是形式化证明专家,也是各种语言编程专家,尤其精通Java和Lean4。

我想用一段Lean4脚本验证:

配置场景:定义了一个线程池,其核心线程数为5、配置了无界阻塞队列、最大线程数为 Integer.MAX_VALUE(约21亿)

验证场景:当任务数量大于线程池的数量时,任务就会卡死,因为后续的节点已经没有线程可用了,对应的任务始终无法完成,因此已经被占用的线程无法释放(因为属于同一个任务)。此后继续提交的新任务会被放到无界阻塞队列中,表现出来就是系统处于假死状态。

请写一段Lean4程序。务必保持简洁。

为降低理解难度,做了一定程度的简化。

import Mathlib.Data.Real.Basic

structure ThreadPool where
  corePoolSize : ℕ
  maxPoolSize : ℕ
  unboundedQueue : Bool := true  -- true 表示无界队列

def isDeadlocked (pool : ThreadPool) (tasks : ℕ) : Prop :=
  pool.corePoolSize < tasks ∧
  pool.unboundedQueue ∧
  pool.maxPoolSize >= pool.corePoolSize

theorem unbounded_queue_causes_deadlock :
  ∃ (pool : ThreadPool) (tasks : ℕ),
    pool.corePoolSize = 5 ∧
    pool.maxPoolSize = 2147483647 ∧
    pool.unboundedQueue = true ∧
    tasks > 5 ∧
    isDeadlocked pool tasks :=
by
  let pool : ThreadPool := { corePoolSize := 5, maxPoolSize := 2147483647, unboundedQueue := true }
  use pool, 6
  simp [isDeadlocked]
  constructor <;> trivial

解释一下:

1. import Mathlib.Data.Real.Basic:导入数学库(本例会使用实数定义,因此需要导入);

2. structure定义线程池结构:

structure ThreadPool where
  corePoolSize : ℕ
  maxPoolSize : ℕ
  unboundedQueue : Bool := true  -- true 表示无界队列

3. 定义死锁条件(符号表示 “与”):

def isDeadlocked (pool : ThreadPool) (tasks : ℕ) : Prop :=
  pool.corePoolSize < tasks ∧
  pool.unboundedQueue ∧
  pool.maxPoolSize > pool.corePoolSize

4. 声明定理和证明

先了解一下声明定理的语法,属于三段式结构:theorem name : Prop := proof

Prop 在我们的代码中对应:

∃ (pool : ThreadPool) (tasks : ℕ),
    pool.corePoolSize = 5 ∧
    pool.maxPoolSize = 2147483647 ∧
    pool.unboundedQueue = true ∧
    tasks > 5 ∧
    isDeadlocked pool tasks

其中的符号表示存在性命题,翻译成人话就是:

存在一个线程池 pool 和任务数 tasks,满足以下所有条件时,会导致死锁(isDeadlocked):
1. 核心线程数 = 5
2. 最大线程数 = Integer.MAX_VALUE
3. 使用无界队列
4. 任务数 > 5

然后构造具体例子,证明命题成立。对应的代码:

  let pool : ThreadPool := { corePoolSize := 5, maxPoolSize := 2147483647, unboundedQueue := true }
  use pool, 6

其中,let 定义具体线程池实例参数,use pool, 6提供例子(核心线程5时,提交6个任务),就可以准备证明了。

在随后的证明过程中,使用simp [isDeadlocked]把 isDeadlocked 的定义展开(类似于内联的概念),并自动把能计算出来的部分(比如 5 < 6)直接化简成 True,让证明变简单。

最后constructor <;> trivial的意思是:把目标拆成多个小条件(constructor),然后逐个(<;>)用“自动验证”(trivial,表示显然会成立的命题)解决。在我们的场景中:

  1. 目标是 5 < 6 ∧ true ∧ 2147483647 > 5。
  2. constructor 将其拆成 3 个子目标:5 < 6true2147483647 > 5
  3. trivial 自动验证这些显然成立的子目标。

最终可以看到,我们定义的定理 unbounded_queue_causes_deadlock ,被成功地证明了:

Lean4 与 Rust

大家可能已经发现,Lean4 与 Rust 这两种编程语言的语法非常相似,其实不仅如此,他们的工具链也很相似:比如 Lean 的版本管理工具elan,类似于 Rust 的 rustup; Lean 的包管理器和构建工具lake ,类似于 Rust 的 cargo)。熟悉 Rust 的同学狂喜,哈哈。

后记

2025年,随着大模型能力的提升,多家模型引入了形式化证明,用来验证大模型解决数学问题的能力,比较常见的训练方法是提供一段形式化代码,并挖去某些内容(或本身就存在待证明的部分),让大模型进行补充,然后验证它补充的内容是否能使得形式化证明通过(参见 DeepSeek又在节前放大招!以及 DeepSeek-Prover-V2:让 AI 学会严谨证明)。这与我们的场景略有差异(我们是直接用大模型生成形式化验证代码,并且想办法用在工程领域)。

在当下的 AI 浪潮中,我们可以借助大模型的能力,在实践过程中不断学习 Lean4 语法,不断构建工程领域的各种“定理”库,并进行开放复用。随着这个库的不断丰富,我们对业务领域的抽象和构建能力也会有所提升。

需要说明的是,本文中的例子非常简单,大家可以作为入门材料参考。在实际的应用中,若要验证某个领域的执行逻辑是否符合预期(如状态机中的状态变化),需要精确理解领域含义和各种动作条件,才能做出明确的抽象,这个过程是比较费力的。不过恰恰在这个过程中,我们能够有机会从具体的业务实现中抽离出来,更为严谨地描述系统行为(从这个角度看,形式化证明与单元测试、集成测试等概念有相似之处)。

另外,单纯靠 AI 写形式化代码会很痛苦,因为很难确认写出来的形式化代码是否正确,导致需要反复修改。因此,有必要熟悉特定形式化语言的语法和编写规范,就好比使用 AI 生成业务代码,需要具备一定的 Java/GoLang 等语言功底。

说了这么多,好像还没提到形式化语言在工程领域的特定落地场景(因为还没完全想清楚)。先别急,我会继续探索 TLA+、Dafny、Coq 等形式化编程语言,看看哪种更适合软件领域的工程化落地,边实践边想。

若有探索形式化方法应用落地的同学,欢迎交流。

学习资源

一些 Lean4 学习资源,包括一些博客文章,以及官方的参考手册。

使用Lean4进行形式化建模(以Java线程池为例)的更多相关文章

  1. Java 线程池框架核心代码分析--转

    原文地址:http://www.codeceo.com/article/java-thread-pool-kernal.html 前言 多线程编程中,为每个任务分配一个线程是不现实的,线程创建的开销和 ...

  2. Java线程池使用说明

    Java线程池使用说明 转自:http://blog.csdn.net/sd0902/article/details/8395677 一简介 线程的使用在java中占有极其重要的地位,在jdk1.4极 ...

  3. (转载)JAVA线程池管理

    平时的开发中线程是个少不了的东西,比如tomcat里的servlet就是线程,没有线程我们如何提供多用户访问呢?不过很多刚开始接触线程的开发攻城师却在这个上面吃了不少苦头.怎么做一套简便的线程开发模式 ...

  4. Java线程池的那些事

    熟悉java多线程的朋友一定十分了解java的线程池,jdk中的核心实现类为java.util.concurrent.ThreadPoolExecutor.大家可能了解到它的原理,甚至看过它的源码:但 ...

  5. 四种Java线程池用法解析

    本文为大家分析四种Java线程池用法,供大家参考,具体内容如下 http://www.jb51.net/article/81843.htm 1.new Thread的弊端 执行一个异步任务你还只是如下 ...

  6. Java线程池的几种实现 及 常见问题讲解

    工作中,经常会涉及到线程.比如有些任务,经常会交与线程去异步执行.抑或服务端程序为每个请求单独建立一个线程处理任务.线程之外的,比如我们用的数据库连接.这些创建销毁或者打开关闭的操作,非常影响系统性能 ...

  7. Java线程池应用

    Executors工具类用于创建Java线程池和定时器. newFixedThreadPool:创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线程.在任意点,在大多数 nThread ...

  8. Java线程池的原理及几类线程池的介绍

    刚刚研究了一下线程池,如果有不足之处,请大家不吝赐教,大家共同学习.共同交流. 在什么情况下使用线程池? 单个任务处理的时间比较短 将需处理的任务的数量大 使用线程池的好处: 减少在创建和销毁线程上所 ...

  9. Java线程池与java.util.concurrent

    Java(Android)线程池 介绍new Thread的弊端及Java四种线程池的使用,对Android同样适用.本文是基础篇,后面会分享下线程池一些高级功能. 1.new Thread的弊端执行 ...

  10. [转 ]-- Java线程池使用说明

    Java线程池使用说明 原文地址:http://blog.csdn.net/sd0902/article/details/8395677 一简介 线程的使用在java中占有极其重要的地位,在jdk1. ...

随机推荐

  1. ANSYS 启动窗口过大问题解决

    方法总结(省流版):选择兼容性下更改高 DPI 设置 => 勾选高DPI 缩放代替 ,且其下对应应用程序选项 1.环境 系统环境:Windows 11 设备情况:分辨率 1920×1080:缩放 ...

  2. 记载火狐浏览器下的一次新手级的js解密工作

    警告:该随笔内容仅用于合法范围下的学习,不得用于任何商业和非法用途,不得未经授权转载,否则后果自负. 首先是需要解密的网站:https://www.aqistudy.cn/historydata/mo ...

  3. 简单实现Android的本地文件读写,暨将List数据保存到Json文件中并读出

    一.让我们从引入依赖开始 //将这两行代码添加到以上位置,其他的一般不用管 implementation 'com.google.code.gson:gson:2.8.5' implementatio ...

  4. DevExpress汉化

    //ini 汉化文件的使用方法: var cxLocalizer1: TcxLocalizer; begin cxLocalizer1.FileName := '你的路径\DevChs.ini'; c ...

  5. vue3第二次传递数据方法无法获取到最新的值

    使用reactive父组件第二次传递给子组件的数据:方法中可以获取到最新数据 <template> <div> <div> <h1>子组件</h1 ...

  6. dify MCP工具调用

    一.概述 前面几篇文章,介绍了Cherry Studio客户端调用MCP,接下来介绍dify如何调用MCP 二.dify插件 需要安装2个插件,分别是:Agent 策略(支持 MCP 工具),MCP ...

  7. 聊聊一体机与AI知识库

    提供AI咨询+AI项目陪跑服务,有需要回复1 之前写了一篇关于一体机的文章: DeepSeek一体机是个什么鬼 一体机产生的原因是春节期间DeepSeek的火爆带动了一些公司的AI需求,但很多公司如医 ...

  8. 40.8K star!让AI帮你读懂整个互联网:Crawl4AI开源爬虫工具深度解析

    嗨,大家好,我是小华同学,关注我们获得"最新.最全.最优质"开源项目和高效工作学习方法 Crawl4AI 是2025年GitHub上最受瞩目的开源网络爬虫工具,专为AI时代设计.它 ...

  9. 从零开始的PHP原生反序列化漏洞

    1.写在前面 OK 兄弟们,这几天一直在面试,发现很多 HR 喜欢问反序列化相关的内容,今天咱们就从最简单的 PHP 原生反序列化入手,带大家入门反序列化 2.PHP 序列化 在 PHP 中,有反序列 ...

  10. 告别 .NET 7,支持将于 5 月结束——我们几乎不认识你

    微软 .NET 7 软件框架的支持将于 5 月结束,这距离其 2022 年发布仅过去 18 个月--这提醒我们,长期更新时代正在成为过去. .NET 7 于 2022 年 11 月 8 日首次亮相,与 ...