对于一个Java程序员而言,能否熟练掌握并发编程是判断他优秀与否的重要标准之一。因为并发编程是Java语言中最为晦涩的知识点,它涉及操作系统、内存、CPU、编程语言等多方面的基础能力,更为考验一个程序员的内功。
那到底应该怎么学习并发编程呢? Java SDK的并发工具包有很多,是要死记硬背每-一个工 具的优缺点和使用场景吗?当然不是,想要学好并发编程,你需要从一个个单一的知识和技术中“跳出来”,高屋建瓴地看问题,并逐步建立自己的知识体系。
可 重 入 锁 ReentrantLock 及 其 他 显 式 锁 相 关 问 题问题一: 跟Synchronized相比, 可重入锁Reentrant Lock其实现原理有什么不同?
其实, 锁的实现原理基本是为了达到一个目的: 让所有的线程都能看到某种标记。Synchronized 通 过 在 对 象头 中 设 置 标 记 实 现 了这 一 目 的 , 是 一 种 JVM 原生的锁实现方式, 而 Reentrant Lock 以及所有的基于 Lock 接口的实现类, 都是通 过用一个 volitile 修饰的 int 型变量, 并保证每个线程 都 能拥 有 对 该 int 的可见性和原子修改, 其本质是基于所谓的 AQS 框架。问题二: 那么请谈谈 AQS 框架是怎么回事儿?
AQS( Abstract Queued Synchronizer 类 ) 是 一 个 用 来 构 建 锁 和 同 步 器的框架, 各种Lock包中的锁( 常用的有Reentrant Lock、ReadWrite Lock) , 以及其他如 Semaphore、 Count Down Latch, 甚至 是 早期的 Future Task 等, 都是基于 AQS 来 构建 。
1. AQS 在 内 部 定 义 了 一 个 volatile int state 变 量 , 表 示 同 步 状 态 : 当 线程调用 lock 方法时 , 如果 state= 0 , 说明没有任何线程占有共享资源的锁, 可以获得锁并将 state= 1 ; 如果 state= 1 , 则说明有线程目前正在使用共享变量, 其他线程必须加入同步队列进行等待。
2. AQS 通 过 Node 内 部 类 构 成 的 一 个 双 向 链 表 结 构 的 同 步 队 列,来完成线程获取锁的排队工作, 当有线程获取锁失败后, 就被添加到队列末尾。
- Node 类 是 对 要 访 问 同 步 代 码 的 线 程 的 封 装 , 包 含 了 线 程 本 身 及 其 状 态 叫wait Status( 有五种不同 取值, 分别表示是否被阻塞, 是否等待唤醒, 是否 已 经 被 取 消 等 ) , 每个 Node 结点关联其prev 结点和next 结 点,方便线程释放锁后快速唤醒下一个在等待的线程, 是一个 FIFO 的过程。
- Node 类 有 两 个 常 量 , SHARED 和 EXCLUSIVE, 分 别 代 表 共 享 模 式 和 独占 模 式 。 所 谓 共 享 模 式 是 一 个 锁 允 许 多 条 线 程 同 时 操 作 ( 信 号 量Semaphore 就 是 基 于 AQS的 共 享 模 式 实 现 的 ) , 独 占 模 式 是 同 一 个 时间 段 只 能 有 一 个 线 程 对 共 享 资 源 进 行 操 作 , 多 余 的 请 求 线 程 需 要 排 队 等 待( 如 Reentran Lock) 。
3. AQS
通 过 内 部 类Condition Object构 建 等 待 队 列 ( 可 有 多 个 ) , 当Condition调 用wait()方 法 后 , 线 程 将 会 加 入 等 待 队 列 中 , 而 当Condition 调 用 signal() 方 法 后 , 线 程 将 从 等 待 队 列 转 移 动 同 步 队 列 中进 行 锁 竞 争 。
4. 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 中的哪些并发工具?
问题六: 请谈谈 Read Write Lock 和 Stamped Lock。
问题七: 如何让 Java 的线程彼此同步? 你了解过哪些同步器? 请分别介绍下。
问题八: Cyclic Barrier和Count Down Latch 看起来很相似, 请对比下呢?
Java 线程池相关问题问题一: Java 中的线程池是如何实现的?
在 Java 中 , 所 谓 的 线 程 池 中 的 “ 线 程 ” , 其 实 是 被 抽 象 为 了 一 个 静 态内 部 类Worker, 它 基 于AQS实 现 , 存 放 在 线 程 池 的Hash Set< Worker> workers 成 员 变 量 中 ;
而 需 要 执 行 的 任 务 则 存 放 在 成 员 变 量work Queue( Blocking Queue< Runnable> work Queue) 中。这 样 , 整 个 线 程 池 实 现 的 基 本 思 想 就 是 : 从work Queue中 不 断 取 出需 要 执 行 的 任 务 , 放 在 Workers 中 进 行 处 理 。问题二: 创建线程池的几个核心构造参数?
Java中 的 线 程 池 的 创 建 其 实 非 常 灵 活 , 我 们 可 以 通 过 配 置 不 同 的 参数, 创建出行为不同的线程池, 这几个参数包括: core Pool Size: 线程池的核心线程数。
- maximum Pool Size: 线程池允许的最大线程数。
- keep Alive Time: 超过核 心线程数 时闲置线 程的存活 时间。
- work Queue: 任 务 执 行 前 保 存 任 务 的 队 列 , 保 存 由 execute 方 法 提 交的 Runnable 任 务 。
问题三: 线程池中的线程是怎么创建的? 是一开始就随着线程池的启动创建好的吗?
显 然 不 是 的 。 线 程 池 默 认 初 始 化 后 不 启 动Worker, 等 待 有 请 求 时 才 启动 。每 当 我 们 调 用execute()方 法 添 加 一 个 任 务 时 , 线 程 池 会 做 如 下 判断 :
如 果 正 在 运 行 的 线 程 数 量 小 于core Pool Size, 那 么 马 上 创 建 线 程 运 行这 个 任 务 ;
如 果 正 在 运 行 的 线 程 数 量 大 于 或 等 于core Pool Size, 那 么 将 这 个 任 务放 入 队 列 ;
如果这 时候队列 满了, 而且正 在运行的 线程数量 小于maximum Pool Size, 那么还 是要创建 非核心线 程立刻运 行这个任 务;
如果队 列满了, 而且正 在运行的 线程数量 大于或等 于maximum Pool Size, 那么线程池会抛出异常Reject Execution Exception。当 一 个 线 程 完 成 任 务 时 , 它 会 从 队 列 中 取 下 一 个 任 务 来 执 行 。当 一 个线 程 无 事 可 做 , 超 过 一 定 的 时 间 ( keep Alive Time) 时 , 线 程 池 会断。
如 果 当 前 运 行 的 线 程 数 大 于core Pool Size, 那么 这个线 程就 被停掉 。所以线程池的所有任务完成后, 它最终会收缩到core Pool Size的大小。
问题四: 既然提到可以通过配置不同参数创建出不同的线程池, 那么Java 中默认实现好的线程池又有哪些呢? 请比较它们的异同。
问题五: 如何在 Java 线程池中提交线程?
Java 内存模型相关问题问题一: 什么是 Java 的内存模型, Java中各个线程是怎么彼此看到对方的变量的?
Java 的内存模型定义了程序中各个变量的访问规则, 即在虚拟机中将变量存储到内存和从内存中取出这样的底层细节。
此处的变量包括实例字段、 静态字段和构成数组对象的元素, 但是不包括局部变量和方法参数, 因为这些是线程私有的, 不会被共享, 所以不存在竞争问题。
Java 中各个线程是怎么彼此看到对方的变量的呢? Java 中定义了主内存与工作内存的概念:
所有的变量都存储在主内存, 每条线程还有自己的工作内存, 保存了被该线程使用到的变量的主内存副本拷贝。线程对变量的所有操作( 读取、 赋值) 都必须在工作内存中进行, 不能直接读写主内存的变量。 不同的线程之间也无法直接访问对方工作内存的变量, 线程间变量值的传递需要通过主存。
问题二: 请谈谈 volatile 有什么特点, 为什么它能保证变量对所有线程的可见性?
问题三: 既然 volatile 能够保证线程间的变量可见性, 是不是就意味着基于volatile 变量的运算就是并发安全的?
问题四: 请对比下 volatile 对比 Synchronized 的异同。
问题五: 请谈谈 Thread Local 是怎么解决并发安全的?
问题六: 很多人都说要慎用 Thread Local, 谈谈你的理解, 使用Thread Local 需要注意些什么?
Synchronized 相关问题
问题一: Synchronized 用过吗, 其原理是什么?
问题二: 你刚才提到获取对象的锁, 这个“ 锁” 到底是什么? 如何确定对象的锁?
问题三: 什么是可重入性, 为什么说 Synchronized 是可重入锁? 可 重 入 性
问题四: JVM 对 Java 的原生锁做了哪些优化?
问题五: 为什么说 Synchronized 是非公平锁?
问题六: 什么是锁消除和锁粗化?
问题七: 为什么说Synchronized是一个悲观锁? 乐观锁的实现原理又是什么? 什么是CAS, 它有什么特性?
问题八: 乐观锁一定就是好的吗?
其他问题
JAVA 并发知识库
JAVA 线程实现/创建方式
4 种线程池
线程生命周期(状态)
终止线程 4 种方式
sleep 与 wait 区别
start 与 run 区别
JAVA 后台线程
JAVA 锁
线程基本方法
线程上下文切换
同步锁与死锁
线程池原理
JAVA 阻塞队列原理
CyclicBarrier、CountDownLatch、Semaphore 的用法
volatile 关键字的作用(变量可见性、禁止重排序)
如何在两个线程之间共享数据
ThreadLocal 作用(线程本地存储)
synchronized 和 ReentrantLock 的区别
ConcurrentHashMap 并发
Java 中用到的线程调度
进程调度算法
什么是 CAS(比较并交换-乐观锁机制-锁自旋)
什么是 AQS(抽象的队列同步器)
所有的并发编程面试题已合成文档,由于篇幅过长,没办法全部上传资料展示
知识脑图:
忍不住想吐槽,知识点多且复杂。。。。。。。。。
快来,一起学习,加入头大家族吧!!