本人虽然不是站在互联网巅峰的男人,但也是混迹江湖十来年的大侠。鄙人接下来就给你们分享一下我当年是如何在并发编程方面从容面对面试官的步步紧逼的!
这是一道 Java 面试中几乎百分百会问到的问题,因为没有任何写过并发程序的开发者会没听说或 者没接触过 Synchronized
。
Synchronized
是由 JVM 实现的一种实现互斥同步的一种方式 , 如果你查看被Synchronized
修 饰过的程序块编译后的字节码,会发现,被Synchronized
修饰过的程序块,在编译前后被编译器 生成了monitorenter
和monitorexit
两个字节码指令 。
这两个指令是什么意思呢?
在虚拟机执行到 monitorenter
指令时 , 首先要尝试获取对象的锁 :
如果这个对象没有锁定 , 或者当前线程已经拥有了这个对象的锁 , 把锁的计数器 +1; 当执行 monitorexit
指令时将锁计数器 -1; 当计数器为 0 时 , 锁就被释放了 。
如果获取对象失败了 , 那当前线程就要阻塞等待 , 直到对象锁被另外一个线程释放为止 。
Java 中 Synchronize
通过在对象头设置标记 , 达到了获取锁和释放锁的目的 。
“ 锁 ” 的 本 质 其 实 是 monitorenter
和 monitorexit
字节码指令 的一个 Reference 类型 的 参 数 , 即 要 锁 定 和 解 锁 的 对 象 。 我 们 知 道 , 使 用 Synchronized
可 以 修 饰 不 同 的 对 象 , 因 此 , 对 应 的 对 象 锁 可 以 这 么 确定 。
Synchronized
明 确 指 定 了 锁 对 象 , 比 如 Synchronized(变量名) 、 Synchronized(this) 等 , 说 明 加 解 锁 对 象 为 该 对 象 。Synchronized
修饰的方法为 非静态方法 , 表示 此方法对应的对象为 锁对象 ;Synchronized
修饰的方法为 静态方法 , 则表示 此方法对应的类对象为 锁对象 。注 意 , 当 一 个 对 象 被 锁 住 时 , 对 象 里 面 所 有 用 Synchronized
修饰 的方法 都 将 产 生 堵 塞 , 而 对 象 里 非 Synchronized
修 饰 的 方 法 可 正 常 被 调用 , 不 受 锁 影 响 。
可重入性 是 锁 的 一 个 基 本 要 求 , 是 为 了 解 决 自 己 锁 死 自 己 的 情 况 。
比 如 , 一 个 类 中 的 同 步 方 法 调 用 另 一 个 同 步 方 法 , 假如 Synchronized
不 支 持 重入 , 进 入 method2 方 法 时 当 前 线 程 获 得 锁 ,method2 方 法 里 面 执 行 method1 时 当 前 线 程 又 要 去 尝 试 获 取 锁 , 这时 如果 不支持 重入 , 它 就 要 等 释 放 , 把 自 己 阻 塞 , 导 致 自 己 锁 死 自 己 。
对 Synchronized
来 说 , 可 重 入 性 是 显 而 易 见 的 , 刚 才 提 到 , 在 执 行 monitorenter
指 令 时 , 如 果 这 个 对 象 没 有 锁 定 , 或 者 当 前 线 程 已 经 拥 有 了 这 个 对 象 的 锁 ( 而 不 是 已 拥 有 了 锁 则 不 能 继 续 获 取
) , 就 把 锁 的 计 数 器 +1, 其 实 本 质 上 就 通 过 这 种 方 式 实 现 了 可 重 入 性 。
在 Java 6 之 前 , Monitor
的 实 现 完 全 依 赖 底 层 操 作 系 统 的 互 斥 锁 来 实 现 , 也 就 是 我 们 刚 才 在 问 题 二 中 所 阐 述 的 获取/释放锁 的 逻 辑 。
由 于 Java 层 面 的 线 程 与 操 作 系 统 的 原 生 线 程 有 映射关系 , 如 果 要 将 一 个 线 程 进 行 阻塞 或 唤起 都 需 要 操 作 系 统 的 协 助 , 这 就 需 要 从 用户态 切 换 到 内核态 来 执 行 , 这 种 切 换 代 价 十 分 昂 贵 , 很 耗 处 理 器 时 间 , 现 代 JDK 中 做 了 大 量 的 优 化 。
一 种 优 化 是 使 用 自旋锁 , 即 在 把 线 程 进 行 阻 塞 操 作 之 前 先 让 线 程 自 旋 等 待 一 段 时 间 , 可 能 在 等 待 期 间 其 他 线 程 已 经 解 锁 , 这 时 就 无 需 再 让 线 程 执 行 阻塞操作 , 避 免 了 用 户 态 到 内 核 态 的 切 换 。
现 代 JDK 中 还 提 供 了 三 种 不 同 的 Monitor
实 现 , 也 就 是 三 种 不 同 的 锁 :
Synchronized
的 运 行 , 当 JVM 检 测 到 不 同 的 竞 争 状 况 时 , 会 自 动 切 换 到 适 合 的 锁 实 现 , 这 就 是 锁 的 升级 、降级 。JVM 会 利 用
CAS
操 作 , 在 对 象 头 上 的Mark Word
部 分 设 置 线 程 ID, 以 表 示 这 个 对 象 偏 向 于 当 前 线 程 , 所 以 并 不 涉 及 真 正 的 互 斥 锁 , 因 为 在 很 多 应 用 场 景 中 , 大 部 分 对 象 生 命 周 期 中 最 多 会 被 一 个 线 程 锁 定 ,使 用 偏 斜 锁 可 以 降 低 无 竞 争 开 销 。
CAS
操 作 Mark Word
来 试 图 获 取 锁 , 如 果 重 试 成 功 ,就 使 用 普 通 的 轻 量 级 锁 ; 否 则 , 进 一 步 升 级 为 重 量 级 锁 。非 公 平 主 要 表 现 在 获 取 锁 的 行 为 上 , 并 非 是 按 照 申 请 锁 的 时 间 前 后 给 等 待 线 程 分 配 锁 的 , 每 当 锁 被 释 放 后 , 任 何 一 个 线 程 都 有 机 会 竞 争 到 锁 ,这 样 做 的 目 的 是 为 了 提 高 执 行 性 能 , 缺 点 是 可 能 会 产 生 线程饥饿现象 。
其 实 , 锁 的 实 现 原 理 基 本 是 为 了 达 到 一 个 目 的 :
让 所 有 的 线 程 都 能 看 到 某 种 标 记 。
Synchronized
通 过 在 对 象 头 中 设 置 标 记 实 现 了 这 一 目 的 , 是 一 种 JVM 原 生 的 锁 实 现 方 式 , 而 ReentrantLock
以 及 所 有 的 基 于 Lock 接 口 的 实 现 类 , 都 是 通 过 用 一 个 volitile
修 饰 的 int 型
变 量 , 并 保 证 每 个 线 程 都 能 拥 有 对 该 int
的 可见性 和 原子 修 改 , 其 本 质 是 基 于 所 谓 的 AQS 框 架 。
AQS(
AbstractQueuedSynchronizer 类
) 是 一 个 用 来 构 建 锁 和 同 步 器 的 框 架 , 各 种 Lock 包 中 的 锁 ( 常 用 的 有ReentrantLock
、ReadWriteLock
) , 以 及 其 他 如Semaphore
、CountDownLatch
, 甚至 是 早 期 的FutureTask
等 , 都 是 基 于 AQS 来 构建。
volatile int state
变 量 , 表 示 同 步 状 态 : 当 线 程 调 用 lock
方 法 时 , 如 果 state=0
, 说 明 没 有 任 何 线 程 占 有 共 享 资 源 的 锁 , 可 以 获 得 锁 并 将 state=1
; 如 果 state=1
, 则 说 明 有 线 程 目 前 正 在 使 用 共 享 变 量 , 其 他 线 程 必 须 加 入 同步队列 进 行 等 待 。Node 内部类
构 成 的 一 个 双 向 链 表 结 构 的 同 步 队 列 , 来 完 成 线程 获 取 锁 的 排 队 工 作 , 当 有 线 程 获 取 锁 失 败 后 , 就 被 添 加 到 队 列 末 尾 。waitStatus
( 有 五 种 不 同 取 值 , 分 别 表 示 是 否 被 阻 塞 , 是 否 等 待 唤 醒 ,是 否 已 经 被 取 消 等 ) , 每 个 Node 结 点 关 联 其 prev 结点 和 next 结点 , 方 便 线 程 释 放 锁 后 快 速 唤 醒 下 一 个 在 等 待 的 线 程 , 是 一 个 FIFO
的 过程 。SHARED
和 EXCLUSIVE
, 分 别 代 表 共享模式 和 独占模式 。 所 谓 共 享 模 式 是 一 个 锁 允 许 多 条 线 程 同 时 操 作 ( 信 号 量 Semaphore
就 是 基 于 AQS 的 共 享 模 式 实 现 的 ) , 独 占 模 式 是 同 一 个 时 间 段 只 能 有 一 个 线 程 对 共 享 资 源 进 行 操 作 , 多 余 的 请 求 线 程 需 要 排 队 等 待( 如 ReentranLock
) 。内部类 ConditionObject
构 建 等 待 队 列 ( 可 有 多 个 ) , 当 Condition
调 用 wait() 方法 后 , 线 程 将 会 加 入 等 待 队 列 中 , 而 当 Condition
调 用 signal() 方法 后 , 线 程 将 从 等 待 队 列 转 移 动 同 步 队 列 中 进 行 锁 竞 争 。Lock
和 Condition
的 时 候 , 其 实 就 是 两 个 队 列 的 互 相 移 动 。ReentrantLock
是 Lock 的 实现类 , 是 一 个 互 斥 的 同 步 锁 。从 功 能 角 度 , ReentrantLock
比 Synchronized
的 同 步 操 作 更 精 细( 因 为 可 以 像 普 通 对 象 一 样 使 用 ) , 甚 至 实 现 Synchronized
没 有 的 高 级 功 能 , 如 :
Synchronized
不 同 , 当 获 取 到 锁 的 线 程 被 中 断 时 , 能 够 响 应 中 断 , 中 断 异 常 将 会 被 抛 出 , 同 时 锁 会 被 释 放 。从 锁 释 放 角 度 , Synchronized
在 JVM 层 面 上 实 现 的 , 不 但 可 以 通 过 一 些 监 控 工 具 监 控 Synchronized
的 锁 定 , 而 且 在 代 码 执 行 出 现 异 常 时 , JVM 会 自 动 释 放 锁 定 ; 但 是 使 用 Lock 则 不 行 , Lock 是 通 过 代 码 实 现 的 , 要 保 证 锁 定 一 定 会 被 释 放 , 就 必 须 将 unLock()
放 到 finally{}
中 。
从 性 能 角 度 , Synchronized
早 期 实 现 比 较 低 效 , 对 比 ReentrantLock
, 大 多 数 场 景 性 能 都 相 差 较 大 。但 是 在 Java 6 中 对 其 进 行 了 非 常 多 的 改 进 , 在 竞 争 不 激 烈 时 ,Synchronized
的 性 能 要 优 于 ReetrantLock
; 在 高 竞 争 情 况 下 ,Synchronized
的 性 能 会 下 降 几 十 倍 , 但 是 ReetrantLock
的 性 能 能 维 持 常 态 。
ReentrantLock
内 部 自 定 义 了 同步器 Sync( Sync 既 实 现 了 AQS,又 实 现 了 AOS, 而 AOS 提 供 了 一 种 互 斥 锁 持 有 的 方 式
) , 其 实 就 是 加 锁 的 时 候 通 过 CAS 算法 , 将 线 程 对 象 放 到 一 个 双 向 链 表 中 , 每 次 获 取 锁 的 时 候 , 看 下 当 前 维 护 的 那 个 线 程 ID 和 当 前 请 求 的 线 程 ID 是 否 一 样 , 一 样 就 可 重 入 了 。
通 常 所 说 的 并发包( JUC) 也 就 是 java.util.concurrent
及 其 子 包 , 集 中 了 Java 并 发 的 各 种 基 础 工 具 类 , 具 体 主 要 包 括 几 个 方 面 :
CountDownLatch
、 CyclicBarrier
、 Semaphore
等 , 比 Synchronized
更 加 高 级 , 可 以 实 现 更 加 丰 富 多 线 程 操 作 的 同 步 结 构 。ConcurrentHashMap
、 有 序 的 ConcunrrentSkipListMap
, 或 者 通 过 类 似 快 照 机 制 实 现 线 程 安 全 的 动 态 数 组 CopyOnWriteArrayList
等 , 各 种 线 程 安 全 的 容 器 。ArrayBlockingQueue
、 SynchorousQueue
或 针 对 特 定 场 景 的 PriorityBlockingQueue
等 , 各 种 并 发 队 列 实 现 。Worker
, 它 基 于 AQS 实 现 , 存 放 在 线 程 池 的 HashSet workers
成 员 变 量 中 ;BlockingQueue workQueue
) 中 。这 样 , 整 个 线 程 池 实 现 的 基 本 思 想 就 是 : 从 workQueue
中 不 断 取 出需 要 执 行 的 任 务 , 放 在 Workers
中 进 行 处 理 。Java 中 的 线 程 池 的 创 建 其 实 非 常 灵 活 , 我 们 可 以 通 过 配 置 不 同 的 参 数 , 创 建 出 行 为 不 同 的 线 程 池 , 这 几 个 参 数 包 括 :
Runnable
任 务 。显 然 不 是 的 。 线 程 池 默 认 初 始 化 后 不 启 动 Worker, 等 待 有 请 求 时 才 启 动 。
每 当 我 们 调 用 execute() 方法 添 加 一 个 任 务 时 , 线 程 池 会 做 如 下 判 断 :
corePoolSize
, 那 么 马 上 创 建 线 程 运 行 这 个 任 务 ;corePoolSize
, 那 么 将 这 个 任 务 放 入 队 列 ;maximumPoolSize
, 那 么 还 是 要 创 建 非 核 心 线 程 立 刻 运 行 这 个 任 务 ;maximumPoolSize
, 那 么 线 程 池 会 抛 出 异 常 RejectExecutionException
。当 一 个 线 程 完 成 任 务 时 , 它 会 从 队 列 中 取 下 一 个 任 务 来 执 行 。 当 一 个 线 程 无 事 可 做 , 超 过 一 定 的 时 间 ( keepAliveTime
) 时 , 线 程 池 会 判 断 。
如 果 当 前 运 行 的 线 程 数 大 于 corePoolSize
, 那 么 这 个 线 程 就 被 停 掉 。所 以 线 程 池 的 所 有 任 务 完 成 后 , 它 最 终 会 收 缩 到 corePoolSize
的 大 小 。
这 个 线 程 池 只 有 一 个 核 心 线 程 在 工 作 , 也 就 是 相 当 于 单 线 程 串 行 执 行 所 有 任 务 。 如 果 这 个 唯 一 的 线 程 因 为 异 常 结 束 , 那 么 会 有 一 个 新 的 线 程 来 替 代 它 。 此 线 程 池 保 证 所 有 任 务 的 执 行 顺 序 按 照 任 务 的 提 交 顺 序 执 行 。
new LinkedBlockingQueue()
, 其 缓 冲 队 列 是 无 界 的 。FixedThreadPool 是 固 定 大 小 的 线 程 池 , 只 有 核 心 线 程 。 每 次 提 交 一 个 任 务 就 创 建 一 个 线 程 , 直 到 线 程 达 到 线 程 池 的 最 大 大 小 。 线 程 池 的 大 小 一 旦 达 到 最 大 值 就 会 保 持 不 变 , 如 果 某 个 线 程 因 为 执 行 异 常 而 结 束 , 那 么 线 程 池 会 补 充 一 个 新 线 程 。
FixedThreadPool 多 数 针 对 一 些 很 稳 定 很 固 定 的 正 规 并 发 线 程 , 多 用 于 服 务 器 。
nThreads
nThreads
new LinkedBlockingQueue()
, 其 缓 冲 队 列是 无 界 的 。CachedThreadPool
是 无 界 线 程 池 , 如 果 线 程 池 的 大 小 超 过 了 处 理 任 务 所 需 要 的 线 程 , 那 么 就 会 回 收 部 分 空 闲 ( 60 秒 不 执 行 任 务 ) 线 程 , 当 任 务 数 增 加 时 , 此 线 程 池 又 可 以 智 能 的 添 加 新 线 程 来 处 理 任 务 。
线 程 池 大 小 完 全 依 赖 于 操 作 系 统 ( 或 者 说 JVM) 能 够 创 建 的 最 大 线 程 大 小 。 SynchronousQueue
是 一 个 是 缓 冲 区 为 1 的 阻 塞 队 列 。
缓 存 型 池 子 通 常 用 于 执 行 一 些 生 存 期 很 短 的 异 步 型 任 务 , 因 此 在 一 些 面 向 连 接 的 daemon
型 SERVER
中 用 得 不 多 。 但 对 于 生 存 期 短 的 异 步 任 务 , 它 是 Executor
的 首 选 。
Integer.MAX_VALUE
new SynchronousQueue()
, 一 个 是 缓 冲 区 为 1 的 阻 塞 队 列 。ScheduledThreadPool
: 核 心 线 程 池 固 定 , 大 小 无 限 的 线 程 池 。 此 线 程 池 支 持 定 时 以 及 周 期 性 执 行 任 务 的 需 求 。 创 建 一 个 周 期 性 执 行 任 务 的 线 程 池 。 如 果 闲 置 , 非 核 心 线 程 池 会 在 DEFAULT_KEEPALIVEMILLIS
时 间 内 回 收 。
corePoolSize
Integer.MAX_VALUE
DEFAULT_KEEPALIVE_MILLIS
new DelayedWorkQueue()
Java 的 内 存 模 型 定 义 了 程 序 中 各 个 变 量 的 访 问 规 则 , 即 在 虚 拟 机 中 将 变 量 存 储 到 内 存 和 从 内 存 中 取 出 这 样 的 底 层 细 节 。
此 处 的 变 量 包 括 实 例 字 段 、 静 态 字 段 和 构 成 数 组 对 象 的 元 素 , 但 是 不 包 括 局 部 变 量 和 方 法 参 数 , 因 为 这 些 是 线 程 私 有 的 , 不 会 被 共 享 , 所 以 不 存 在 竞 争 问 题 。
Java 中 各 个 线 程 是 怎 么 彼 此 看 到 对 方 的 变 量 的 呢 ? Java 中 定 义 了 主 内 存 与 工 作 内 存 的 概 念 :
所 有 的 变 量 都 存 储 在 主 内 存 , 每 条 线 程 还 有 自 己 的 工 作 内 存 , 保 存 了 被 该 线 程 使 用 到 的 变 量 的 主 内 存 副 本 拷 贝 。
线 程 对 变 量 的 所 有 操 作 ( 读 取 、 赋 值 ) 都 必 须 在 工 作 内 存 中 进 行 , 不 能 直 接 读 写 主 内 存 的 变 量 。 不 同 的 线 程 之 间 也 无 法 直 接 访 问 对 方 工 作 内 存 的 变 量 , 线 程 间 变 量 值 的 传 递 需 要 通 过 主 内 存 。
关 键 字 volatile
是 Java 虚拟机 提 供 的 最 轻 量 级 的 同 步 机 制 。 当 一 个 变 量 被 定 义 成 volatile
之 后 , 具 备 两 种 特 性 :
Java 的 内 存 模 型 定 义 了 8 种 内 存 间 操 作 :
volatile 的 实 现 基 于 这 8 种 内 存 间 操 作 , 保 证 了 一 个 线 程 对 某 个 volatile 变 量 的 修 改 , 一 定 会 被 另 一 个 线 程 看 见 , 即 保 证 了 可 见 性 。
Synchronized 既 能 保 证 可 见 性 , 又 能 保 证 原 子 性 , 而 volatile 只 能 保 证 可 见 性 , 无 法 保 证 原 子 性 。
ThreadLocal
和 Synchonized
都 用 于 解 决 多 线 程 并 发 访 问 , 防 止 任 务 在 共 享 资 源 上 产 生 冲 突 。 但 是 ThreadLocal
与 Synchronized
有 本 质 的 区 别 。
Synchronized
用 于 实 现 同 步 机 制 , 是 利 用 锁 的 机 制 使 变 量 或 代 码 块 在 某 一 时 该 只 能 被 一 个 线 程 访 问 , 是 一 种 “ 以 时 间 换 空 间 ” 的 方 式 。
而 ThreadLocal
为 每 一 个 线 程 都 提 供 了 变 量 的 副 本 , 使 得 每 个 线 程 在 某 一 时 间 访 问 到 的 并 不 是 同 一 个 对 象 , 根 除 了 对 变 量 的 共 享 , 是 一 种 “ 以 空 间 换 时 间 ” 的 方 式 。
使 用 ThreadLocal 要 注 意 remove!
ThreadLocal 的 实 现 是 基 于 一 个 所 谓 的 ThreadLocalMap
, 在 ThreadLocalMap
中 , 它 的 key 是 一 个 弱 引 用 。
通 常 弱 引 用 都 会 和 引 用 队 列 配 合 清 理 机 制 使 用 , 但 是 ThreadLocal 是 个 例 外 , 它 并 没 有 这 么 做 。
这 意 味 着 , 废 弃 项 目 的 回 收 依 赖 于 显 式 地 触 发 , 否 则 就 要 等 待 线 程 结 束 , 进 而 回 收 相 应 ThreadLocalMap
! 这 就 是 很 多 OOM 的 来 源 , 所 以 通 常 都 会 建 议 , 应 用 一 定 要 自 己 负 责 remove, 并 且 不 要 和 线 程 池 配 合 , 因 为 worker 线 程 往 往 是 不 会 退 出 的 。
以上便是鄙人所总结的面经中的一部分,具体有多少,我直接上图!
需要下图中的面经资源的话,记得关注博主,然后添加助理VX:C18173184271
免费获取!!!