并发编程面试 (一)Synchronized 相关

并发编程面试(一)Synchronized 相关

Synchronized 原 理 是 什 么 ?

Synchronized 是 由 JVM 实 现 的 一 种 实 现 互 斥 同 步 的 一 种 方 式 , 如 果 你 查 看 被 Synchronized 修 饰 过 的 程 序 块 编 译 后 的 字 节 码 , 会 发 现 , 被 Synchronized 修 饰 过 的 程 序 块 , 在 编 译 前 后 被 编 译 器 生 成 了 monitorenter 和 monitorexit 两 个 字 节 码 指 令 。

这 两 个 指 令 是 什 么 意 思 呢 ?

在 虚 拟 机 执 行 到 monitorenter 指 令 时 , 首 先 要 尝 试 获 取 对 象 的 锁 :

如 果 这 个 对 象 没 有 锁 定 , 或 者 当 前 线 程 已 经 拥 有 了 这 个 对 象 的 锁 , 把 锁 的 计 数 器 + 1 ; 当 执 行 monitorexit 指 令 时 将 锁 计 数 器 - 1 ; 当 计 数 器 为 0 时 , 锁 就 被 释 放 了 。

如 果 获 取 对 象 失 败 了 , 那 当 前 线 程 就 要 阻 塞 等 待 , 直 到 对 象 锁 被 另 外 一 个 线 程 释 放 为 止 。

Java 中 Synchronize 通 过 在 对 象 头 设 置 标 记 , 达 到 了 获 取 锁 和 释 放 锁 的 目 的 。

Synchronized 这 个 “ 锁 ” 到 底 是 什 么 ? 如 何 确 定 对 象 的 锁 ?

“ 锁 ” 的 本 质 其 实 是 monitorenter 和 monitorexit 字 节 码 指 令 的 一 个 Reference 类 型 的 参 数 , 即 要 锁 定 和 解 锁 的 对 象 。 我 们 知 道 , 使 用Synchronized 可 以 修 饰 不 同 的 对 象 , 因 此 , 对 应 的 对 象 锁 可 以 这 么 确定 。

  • 如 果 Synchronized 明 确 指 定 了 锁 对 象 , 比 如 Synchronized( 变 量 名 ) 、 Synchronized( this) 等 , 说 明 加 解 锁 对 象 为 该 对 象 。

  • 如 果 没 有 明 确 指 定 :

    若 Synchronized 修 饰 的 方 法 为 非 静 态 方 法 , 表 示 此 方 法 对 应 的 对 象 为 锁 对 象 ;

    若Synchronized 修 饰 的 方 法 为 静 态 方 法 , 则 表 示 此 方 法 对 应 的 类 对 象 为 锁 对 象 。

  • 注 意 , 当 一 个 对 象 被 锁 住 时 , 对 象 里 面 所 有 用 Synchronized 修 饰 的 方 法 都 将 产 生 堵 塞 , 而 对 象 里 非 Synchronized 修 饰 的 方 法 可 正 常 被 调 用 , 不 受 锁 影 响 。

什 么 是 可 重 入 性 , 为 什 么 说 Synchronized 是 可 重 入 锁 ?

可 重 入 性 是 锁 的 一 个 基 本 要 求 , 是 为 了 解 决 自 己 锁 死 自 己 的 情 况 。

比 如 一 个 类 中 的 同 步 方 法 调 用 另 一 个 同 步 方 法 ,

假 如Synchronized 不 支 持 重 入 , 进 入 method 2 方 法 时 当 前 线 程 获 得 锁 , method 2 方 法 里 面 执 行 method 1 时 当 前 线 程 又 要 去 尝 试 获 取 锁 , 这 时 如 果 不 支 持 重 入 , 它 就 要 等 释 放 , 把 自 己 阻 塞 , 导 致 自 己 锁 死 自 己 。

对 Synchronized 来 说 , 可 重 入 性 是 显 而 易 见 的 , 刚 才 提 到 , 在 执 行monitorenter 指 令 时 , 如 果 这 个 对 象 没 有 锁 定 , 或 者 当 前 线 程 已 经 拥有 了 这 个 对 象 的 锁 ( 而 不 是 已 拥 有 了 锁 则 不 能 继 续 获 取 ) , 就 把 锁 的 计数 器 + 1 , 其 实 本 质 上 就 通 过 这 种 方 式 实 现 了 可 重 入 性 。

JVM 对 Java 的 原 生 锁 做 了 哪 些 优 化 ?

在 Java 6 之 前 , Monitor 的 实 现 完 全 依 赖 底 层 操 作 系 统 的 互 斥 锁 来 实 现 , 也 就 是 我 们 刚 才 在 问 题 二 中 所 阐 述 的 获 取 / 释 放 锁 的 逻 辑 。

由 于 Java 层 面 的 线 程 与 操 作 系 统 的 原 生 线 程 有 映 射 关 系 , 如 果 要 将 一 个 线 程 进 行 阻 塞 或 唤 起 都 需 要 操 作 系 统 的 协 助 , 这 就 需 要 从 用 户 态 切 换 到 内 核 态 来 执 行 , 这 种 切 换 代 价 十 分 昂 贵 , 很 耗 处 理 器 时 间 , 现 代 JDK 中 做 了 大 量 的 优 化 。

一 种 优 化 是 使 用 自 旋 锁 , 即 在 把 线 程 进 行 阻 塞 操 作 之 前 先 让 线 程 自 旋 等 待 一 段 时 间 , 可 能 在 等 待 期 间 其 他 线 程 已 经 解 锁 , 这 时 就 无 需 再 让 线 程 执 行 阻 塞 操 作 , 避 免 了 用 户 态 到 内 核 态 的 切 换 。

现 代 JDK 中 还 提 供 了 三 种 不 同 的 Monitor 实 现 , 也 就 是 三 种 不 同 的 锁 :

· 偏 向 锁 ( Biased Locking)

· 轻 量 级 锁

· 重 量 级 锁

这 三 种 锁 使 得 JDK 得 以 优 化 Synchronized 的 运 行 , 当 JVM 检 测 到 不 同 的 竞 争 状 况 时 , 会 自 动 切 换 到 适 合 的 锁 实 现 , 这 就 是 锁 的 升 级 、 降 级 。

· 当 没 有 竞 争 出 现 时 , 默 认 会 使 用 偏 向 锁 。

JVM 会 利 用 CAS 操 作 , 在 对 象 头 上 的 Mark Word 部 分 设 置 线 程ID, 以 表 示 这 个 对 象 偏 向 于 当 前 线 程 , 所 以 并 不 涉 及 真 正 的 互 斥 锁 , 因 为 在 很 多 应 用 场 景 中 , 大 部 分 对 象 生 命 周 期 中 最 多 会 被 一 个 线 程 锁 定 , 使 用 偏 斜 锁 可 以 降 低 无 竞 争 开 销 。

· 如 果 有 另 一 线 程 试 图 锁 定 某 个 被 偏 斜 过 的 对 象 , JVM就 撤 销 偏 斜 锁 , 切 换 到 轻 量 级 锁 实 现

· 轻 量 级 锁 依 赖 CAS 操 作 Mark Word 来 试 图 获 取 锁 , 如 果 重 试 成 功 , 就 使 用 普 通 的 轻 量 级 锁 ; 否 则 , 进 一 步 升 级 为 重 量 级 锁 。

为 什 么 说 Synchronized 是 非 公 平 锁 ?

非 公 平 主 要 表 现 在 获 取 锁 的 行 为 上 , 并 非 是 按 照 申 请 锁 的 时 间 前 后 给 等 待 线 程 分 配 锁 的 , 每 当 锁 被 释 放 后 , 任 何 一 个 线 程 都 有 机 会 竞 争 到 锁 , 这 样 做 的 目 的 是 为 了 提 高 执 行 性 能 , 缺 点 是 可 能 会 产 生 线 程 饥 饿 现 象 。

什 么 是 锁 消 除 和 锁 粗 化 ?

· 锁 消 除 : 指 虚 拟 机 即 时 编 译 器 在 运 行 时 , 对 一 些 代 码 上 要 求 同 步 , 但 被 检 测 到 不 可 能 存 在 共 享 数 据 竞 争 的 锁 进 行 消 除 。 主 要 根 据 逃 逸 分 析 。

程 序 员 怎 么 会 在 明 知 道 不 存 在 数 据 竞 争 的 情 况 下 使 用 同 步 呢 ? 很 多 不 是 程 序 员 自 己 加 入 的

· 锁 粗 化 : 原 则 上 , 同 步 块 的 作 用 范 围 要 尽 量 小 。 但 是 如 果 一 系 列 的 连 续 操 作 都 对 同 一 个 对 象 反 复 加 锁 和 解 锁 , 甚 至 加 锁 操 作 在 循 环 体 内 , 频 繁 地 进 行 互 斥 同 步 操 作 也 会 导 致 不 必 要 的 性 能 损 耗 。

锁 粗 化 就 是 增 大 锁 的 作 用 域 。

为 什 么 说 Synchronized 是 一 个 悲 观 锁 ? 乐 观 锁 的 实 现 原 理

又 是 什 么 ? 什 么 是 CAS, 它 有 什 么 特 性 ?

Synchronized 显 然 是 一 个 悲 观 锁 , 因 为 它 的 并 发 策 略 是 悲 观 的 :

不 管 是 否 会 产 生 竞 争 , 任 何 的 数 据 操 作 都 必 须 要 加 锁 、 用 户 态 核 心 态 转 换 、 维 护 锁 计 数 器 和 检 查 是 否 有 被 阻 塞 的 线 程 需 要 被 唤 醒 等 操 作 。

随 着 硬 件 指 令 集 的 发 展 , 我 们 可 以 使 用 基 于 冲 突 检 测 的 乐 观 并 发 策 略 。 先 进 行 操 作 , 如 果 没 有 其 他 线 程 征 用 数 据 , 那 操 作 就 成 功 了 ;

如 果 共 享 数 据 有 征 用 , 产 生 了 冲 突 , 那 就 再 进 行 其 他 的 补 偿 措 施 。 这 种 乐 观 的 并 发 策 略 的 许 多 实 现 不 需 要 线 程 挂 起 , 所 以 被 称 为 非 阻 塞 同 步 。 乐 观 锁 的 核 心 算 法 是 CAS( Compareand Swap, 比 较 并 交 换 ) , 它 涉 及 到 三 个 操 作 数 : 内 存 值 、 预 期 值 、 新 值 。 当 且 仅 当 预 期 值 和 内 存 值 相 等 时 才 将 内 存 值 修 改 为 新 值 。

这 样 处 理 的 逻 辑 是 , 首 先 检 查 某 块 内 存 的 值 是 否 跟 之 前 我 读 取 时 的 一样 , 如 不 一 样 则 表 示 期 间 此 内 存 值 已 经 被 别 的 线 程 更 改 过 , 舍 弃 本 次 操 作 , 否 则 说 明 期 间 没 有 其 他 线 程 对 此 内 存 值 操 作 , 可 以 把 新 值 设 置 给 此 块 内 存 。

CAS 具 有 原 子 性 , 它 的 原 子 性 由 CPU 硬 件 指 令 实 现 保 证 , 即 使 用 JNI 调 用 Native 方 法 调 用 由 C++ 编 写 的 硬 件 级 别 指 令 , JDK 中 提 供 了 Unsafe 类 执 行 这 些 操 作 。

乐 观 锁 一 定 就 是 好 的 吗 ?

乐 观 锁 避 免 了 悲 观 锁 独 占 对 象 的 现 象 , 同 时 也 提 高 了 并 发 性 能 , 但 它 也 有 缺 点 :

  1. 乐 观 锁 只 能 保 证 一 个 共 享 变 量 的 原 子 操 作 。 如 果 多 一 个 或 几 个 变 量 , 乐观 锁 将 变 得 力 不 从 心 , 但 互 斥 锁 能 轻 易 解 决 , 不 管 对 象 数 量 多 少 及 对 象 颗 粒 度 大 小 。

  2. 长 时 间 自 旋 可 能 导 致 开 销 大 。 假 如 CAS 长 时 间 不 成 功 而 一 直 自 旋 , 会 给 CPU 带 来 很 大 的 开 销 。

  3. ABA问 题 。 CAS 的 核 心 思 想 是 通 过 比 对 内 存 值 与 预 期 值 是 否 一 样 而 判 断 内 存 值 是 否 被 改 过 , 但 这 个 判 断 逻 辑 不 严 谨 , 假 如 内 存 值 原 来 是A,后 来 被 一 条 线 程 改 为 B, 最 后 又 被 改 成 了 A, 则 CAS 认 为 此 内 存 值 并 没 有 发 生 改 变 , 但 实 际 上 是 有 被 其 他 线 程 改 过 的 , 这 种 情 况 对 依 赖 过 程 值 的 情 景 的 运 算 结 果 影 响 很 大 。 解 决 的 思 路 是 引 入 版 本 号 , 每 次 变 量 更 新 都 把 版 本 号 加 一 。

你可能感兴趣的:(java)