并发编程面试 (二)ReentrantLock 相关

601 阅读9分钟

跟Synchronized 相 比 , 可 重 入 锁 Reentrant Lock 其 实 现 原 理 有 什 么 不 同 ?

其 实 , 锁 的 实 现 原 理 基 本 是 为 了 达 到 一 个 目 的 : 让 所 有 的 线 程 都 能 看 到 某 种 标 记 。Synchronized 通 过 在 对 象 头 中 设 置 标 记 实 现 了 这 一 目 的 , 是 一 种 JVM 原 生 的 锁 实 现 方 式 , 而 Reentrant Lock 以 及 所 有 的 基 于 Lock 接 口 的 实 现 类 , 都 是 通 过 用 一 个 volitile 修 饰 的 int 型 变 量 , 并 保 证 每 个 线 程 都 能 拥 有 对 该 int 的 可 见 性 和 原 子 修 改 , 其 本 质 是 基 于 所 谓 的 AQS 框 架 。

AQS 框 架 是 怎 么 回 事 儿

AQS( Abstract Queued Synchronizer 类 ) 是 一 个 用 来 构 建 锁 和 同 步 器 的 框 架 , 各 种 Lock 包 中 的 锁 ( 常 用 的 有 Reentrant Lock、 Read Write Lock) , 以 及 其 他 如 Semaphore、CountDownLatch, 甚 至 是 早 期 的 Future Task 等 , 都 是 基 于 AQS 来 构 建 。

  • AQS 在 内 部 定 义 了 一 个 volatile int state 变 量 , 表 示 同 步 状 态 : 当 线 程 调 用 lock 方 法 时 , 如 果 state= 0 , 说 明 没 有 任 何 线 程 占 有 共 享 资 源的 锁 , 可 以 获 得 锁 并 将 state= 1 ; 如 果 state= 1 , 则 说 明 有 线 程 目 前 正 在 使 用 共 享 变 量 , 其 他 线 程 必 须 加 入 同 步 队 列 进 行 等 待 。

  • AQS 通 过 Node 内 部 类 构 成 的 一 个 双 向 链 表 结 构 的 同 步 队 列 , 来 完 成 线 程 获 取 锁 的 排 队 工 作 , 当 有 线 程 获 取 锁 失 败 后 , 就 被 添 加 到 队 列 末 尾 。

  • Node 类 是 对 要 访 问 同 步 代 码 的 线 程 的 封 装 , 包 含 了 线 程 本 身 及 其 状 态 叫 wait Status( 有 五 种 不同取 值 ,分 别 表 示 是 否 被 阻 塞 , 是 否 等 待 唤 醒 , 是 否 已 经 被 取 消 等 ) , 每 个 Node 结 点 关 联 其 prev 结 点 和 next 结 点 , 方 便 线 程 释 放 锁 后 快 速 唤 醒 下 一 个 在 等 待 的 线 程 , 是 一 个 FIFO 的 过 程 。

  • Node 类 有 两 个 常 量 , SHARED 和 EXCLUSIVE, 分 别 代 表 共 享 模 式 和 独 占 模 式 。 所 谓 共 享 模 式 是 一 个 锁 允 许 多 条 线 程 同 时 操 作 ( 信 号 量Semaphore 就 是 基 于 AQS 的 共 享 模 式 实 现 的 ) , 独 占 模 式 是 同 一 个 时 间 段 只 能 有 一 个 线 程 对 共 享 资 源 进 行 操 作 , 多 余 的 请 求 线 程 需 要 排 队 等 待( 如 Reentran Lock) 。

  • AQS 通 过 内 部 类 Condition Object 构 建 等 待 队 列 ( 可 有 多 个 ) , 当Condition 调 用 wait()方 法 后 , 线 程 将 会 加 入 等 待 队 列 中 , 而 当Condition 调 用 signal() 方 法 后 , 线 程 将 从 等 待 队 列 转 移 动 同 步 队 列 中进 行 锁 竞 争 。

  • AQS 和 Condition 各 自 维 护 了 不 同 的 队 列 , 在 使 用 Lock 和Condition 的 时 候 , 其 实 就 是 两 个 队 列 的 互 相 移 动 。

请 尽 可 能 详 尽 地 对 比 下 Synchronized 和 Reentrant Lock

Reentrant Lock 是 Lock 的 实 现 类 , 是 一 个 互 斥 的 同 步 锁 。

从 功 能 角 度 , Reentrant Lock 比 Synchronized 的 同 步 操 作 更 精 细

( 因 为 可 以 像 普 通 对 象 一 样 使 用 ) , 甚 至 实 现 Synchronized 没 有 的 高 级 功 能 , 如 :

  • 等 待 可 中 断 : 当 持 有 锁 的 线 程 长 期 不 释 放 锁 的 时 候 , 正 在 等 待 的 线 程 可 以 选 择 放 弃 等 待 , 对 处 理 执 行 时 间 非 常 长 的 同 步 块 很 有 用 。

  • 带 超 时 的 获 取 锁 尝 试 : 在 指 定 的 时 间 范 围 内 获 取 锁 , 如 果 时 间 到 了 仍 然 无 法 获 取 则 返 回 。

  • 可 以 判 断 是 否 有 线 程 在 排 队 等 待 获 取 锁 。

  • 可 以 响 应 中 断 请 求 : 与 Synchronized 不 同 , 当 获 取 到 锁 的 线 程 被 中 断 时 , 能 够 响 应 中 断 , 中 断 异 常 将 会 被 抛 出 , 同 时 锁 会 被 释 放 。

  • 可 以 实 现 公 平 锁 。

从 锁 释 放 角 度 , Synchronized 在 JVM 层 面 上 实 现 的 , 不 但 可 以 通 过 一 些 监 控 工 具 监 控 Synchronized 的 锁 定 , 而 且 在 代 码 执 行 出 现 异 常 时 , JVM 会 自 动 释 放 锁 定 ; 但 是 使 用 Lock 则 不 行 , Lock 是 通 过 代码 实 现 的 , 要 保 证 锁 定 一 定 会 被 释 放 , 就 必 须 将 un Lock() 放 到f inally{} 中 。

从 性 能 角 度 , Synchronized 早 期 实 现 比 较 低 效 , 对 比Reentrant Lock, 大 多 数 场 景 性 能 都 相 差 较 大 。但 是 在 Java 6 中 对 其 进 行 了 非 常 多 的 改 进 , 在 竞 争 不 激 烈 时 , Synchronized 的 性 能 要 优 于 Reetrant Lock; 在 高 竞 争 情 况 下 , Synchronized 的 性 能 会 下 降 几 十 倍 , 但 是 Reetrant Lock 的 性 能 能 维 持 常 态 。

Reentrant Lock 是 如 何 实 现 可 重 入 性 的 ?

Reentrant Lock 内 部 自 定 义 了 同 步 器 Sync( Sync 既 实 现 了 AQS, 又 实 现 了 AOS, 而 AOS 提 供 了 一 种 互 斥 锁 持 有 的 方 式 ) , 其 实 就 是 加 锁 的 时 候 通 过 CAS 算 法 , 将 线 程 对 象 放 到 一 个 双 向 链 表 中 , 每 次 获 取 锁 的 时 候 , 看 下 当 前 维 护 的 那 个 线 程 ID 和 当 前 请 求 的 线 程 ID 是 否 一 样 , 一 样 就 可 重 入 了 。

除 了 Reetrant Lock, 你 还 接 触 过 JUC 中 的 哪 些 并 发 工 具 ?

通 常 所 说 的 并 发 包 ( JUC) 也 就 是 java. util. concurrent 及 其 子 包 , 集 中 了 Java 并 发 的 各 种 基 础 工 具 类 , 具 体 主 要 包 括 几 个 方 面 :

  • 提 供 了 Count Down Latch、 Cyclic Barrier、 Semaphore 等 , 比 Synchronized 更 加 高 级 , 可 以 实 现 更 加 丰 富 多 线 程 操 作 的 同 步 结 构 。

  • 提 供 了 Concurrent Hash Map、 有 序 的 Concunrrent Skip List Map, 或 者 通 过 类 似 快 照 机 制 实 现 线 程 安 全 的 动 态 数 组 Copy On Write Array List 等 , 各 种 线 程 安 全 的 容 器 。

  • ·提 供 了 Array Blocking Queue、 Synchorous Queue 或 针 对 特 定 场 景 的Priority Blocking Queue 等 , 各 种 并 发 队 列 实 现 。

  • 强 大 的 Executor 框 架 , 可 以 创 建 各 种 不 同 类 型 的 线 程 池 , 调 度 任 务 运 行 等 。

请 谈 谈 Read Write Lock 和 Stamped Lock。

虽 然 Reentrant Lock 和 Synchronized 简 单 实 用 , 但 是 行 为 上 有 一 定 局 限 性 , 要 么 不 占 , 要 么 独 占 。 实 际 应 用 场 景 中 , 有 时 候 不 需 要 大 量 竞 争 的 写 操 作 , 而 是 以 并 发 读 取 为 主 , 为 了 进 一 步 优 化 并 发 操 作 的 粒度 , Java 提 供 了 读 写 锁 。

读 写 锁 基 于 的 原 理 是 多 个 读 操 作 不 需 要 互 斥 , 如 果 读 锁 试 图 锁 定 时 , 写 锁 是 被 某 个 线 程 持 有 , 读 锁 将 无 法 获 得 , 而 只 好 等 待 对 方 操 作 结 束 , 这 样 就 可 以 自 动 保 证 不 会 读 取 到 有 争 议 的 数 据 。

Read Write Lock 代 表 了 一 对 锁 , 下 面 是 一 个 基 于 读 写 锁 实 现 的 数 据 结 构 , 当 数 据 量 较 大 , 并 发 读 多 、 并 发 写 少 的 时 候 , 能 够 比 纯 同 步 版 本 凸 显 出 优 势 :

读 写 锁 看 起 来 比 Synchronized 的 粒 度 似 乎 细 一 些 , 但 在 实 际 应 用 中 , 其 表 现 也 并 不 尽 如 人 意 , 主 要 还 是 因 为 相 对 比 较 大 的 开 销 。

所 以 , JDK 在 后 期 引 入 了 Stamped Lock, 在 提 供 类 似 读 写 锁 的 同 时 , 还 支 持 优 化 读 模 式 。 优 化 读 基 于 假 设 , 大 多 数 情 况 下 读 操 作 并 不 会 和 写 操 作 冲 突 , 其 逻 辑 是 先 试 着 修 改 , 然 后 通 过 validate 方 法 确 认 是 否 进 入 了 写 模 式 , 如 果 没 有 进 入 , 就 成 功 避 免 了 开 销 ; 如 果 进 入 , 则 尝 试 获 取 读 锁 。

如 何 让 Java 的 线 程 彼 此 同 步 ? 你 了 解 过 哪 些 同 步 器 ? 请 分 别 介 绍 下 。

JUC 中 的 同 步 器 三 个 主 要 的 成 员 : Count Down Latch、 Cyclic Barrier和 Semaphore, 通 过 它 们 可 以 方 便 地 实 现 很 多 线 程 之 间 协 作 的 功 能 。Count Down Latch 叫 倒 计 数 , 允 许 一 个 或 多 个 线 程 等 待 某 些 操 作 完 成 。 看 几 个 场 景 :

  • 跑 步 比 赛 , 裁 判 需 要 等 到 所 有 的 运 动 员 ( “ 其 他 线 程 ” ) 都 跑 到 终 点( 达 到 目 标 ) , 才 能 去 算 排 名 和 颁 奖 。

  • 模 拟 并 发 , 我 需 要 启 动 100 个 线 程 去 同 时 访 问 某 一 个 地 址 , 我 希 望 它 们 能 同 时 并 发 , 而 不 是 一 个 一 个 的 去 执 行 。

    用 法 : Count Down Latch 构 造 方 法 指 明 计 数 数 量 , 被 等 待 线 程 调 用count Down 将 计 数 器 减 1 , 等 待 线 程 使 用 await 进 行 线 程 等 待 。 一 个 简 单 的 例 子 :

Cyclic Barrier 叫 循 环 栅 栏 , 它 实 现 让 一 组 线 程 等 待 至 某 个 状 态 之 后 再 全 部 同 时 执 行 , 而 且 当 所 有 等 待 线 程 被 释 放 后 , Cyclic Barrier 可 以 被 重 复 使 用 。 Cyclic Barrier 的 典 型 应 用 场 景 是 用 来 等 待 并 发 线 程 结 束 。

Cyclic Barrier 的 主 要 方 法 是 await(), await() 每 被 调 用 一 次 , 计 数 便 会 减 少 1 , 并 阻 塞 住 当 前 线 程 。 当 计 数 减 至 0 时 , 阻 塞 解 除 , 所 有 在 此 Cyclic Barrier 上 面 阻 塞 的 线 程 开 始 运 行 。

在 这 之 后 , 如 果 再 次 调 用 await(), 计 数 就 又 会 变 成 N- 1 , 新 一 轮 重 新 开 始 , 这 便 是 Cyclic 的 含 义 所 在 。 Cyclic Barrier. await() 带 有 返 回 值 , 用 来 表 示 当 前 线 程 是 第 几 个 到 达 这 个 Barrier 的 线 程 。

举 例 说 明 如 下 :

Semaphore, Java 版 本 的 信 号 量 实 现 , 用 于 控 制 同 时 访 问 的 线 程 个数 , 来 达 到 限 制 通 用 资 源 访 问 的 目 的 , 其 原 理 是 通 过 acquire() 获 取 一 个 许 可 , 如 果 没 有 就 等 待 , 而 release() 释 放 一 个 许 可 。

如 果 Semaphore 的 数 值 被 初 始 化 为 1 , 那 么 一 个 线 程 就 可 以 通 过acquire 进 入 互 斥 状 态 , 本 质 上 和 互 斥 锁 是 非 常 相 似 的 。 但 是 区 别 也 非 常 明 显 , 比 如 互 斥 锁 是 有 持 有 者 的 , 而 对 于 Semaphore 这 种 计 数 器 结 构 , 虽 然 有 类 似 功 能 , 但 其 实 不 存 在 真 正 意 义 的 持 有 者 , 除 非 我 们 进 行 扩 展 包 装 。

Cyclic Barrier 和 Count Down Latch 看 起 来 很 相 似 , 请 对 比 下 呢 ?

它 们 的 行 为 有 一 定 相 似 度 , 区 别 主 要 在 于 :

  • Count Down Latch 是 不 可 以 重 置 的 , 所 以 无 法 重 用 , Cyclic Barrier 没 有 这 种 限 制 , 可 以 重 用 。

  • Count Down Latch 的 基 本 操 作 组 合 是 count Down/ await, 调 用 await 的 线 程 阻 塞 等 待 count Down 足 够 的 次 数 , 不 管 你 是 在 一 个 线程 还 是 多 个 线 程 里 count Down, 只 要 次 数 足 够 即 可 。 Cyclic Barrier的 基 本 操 作 组 合 就 是 await, 当 所 有 的 伙 伴 都 调 用 了 await, 才 会 继 续 进 行 任 务 , 并 自 动 进 行 重 置 。

  • Count Down Latch 目 的 是 让 一 个 线 程 等 待 其 他 N 个 线 程 达 到 某 个 条 件 后 , 自 己 再 去 做 某 个 事 ( 通 过 Cyclic Barrier 的 第 二 个 构 造 方 法 public Cyclic Barrier( int parties, Runnable barrier Action), 在 新 线 程 里 做 事 可 以 达 到 同 样 的 效 果 ) 。 而 Cyclic Barrier 的 目 的 是 让 N 多 线 程 互 相 等 待 直 到 所 有 的 都 达 到 某 个 状 态 , 然 后 这 N 个 线 程 再 继 续 执 行 各 自 后 续 ( 通 过 Count Down Latch 在 某 些 场 合 也 能 完 成 类 似 的 效 果 ) 。