Java多线程的面试题

123 阅读11分钟

欢迎关注WX公众号:“程序猿补课班”,分享Java相关技术知识,学习经验,面试经验等。小伙伴快来补课吧!

1.sleep(),wait()的区别?

sleep是线程类(Thread)的方法,
wait是Object类的方法,
sleep方法并不会释放锁,即使当前线程使用sleep方法让出了cpu,但其他被同步锁挡住了的线程也无法得到执行。
wait是指在一个已经进入了同步锁的线程内,让自己暂时让出同步锁,以便其他正在等待此锁的线程可以得到同步锁并运行,
只有其他线程调用了notify方法(notify并不释放锁,只是告诉调用过wait方法的线程可以去参与获得锁的竞争了,但不是马上得到锁,因为锁还在别人手里,别人还没释放。

2.Java 中怎样唤醒一个阻塞的线程?

如果线程是因为调用了 wait()、sleep()或者 join()方法而导致的阻塞,可以
中断线程,并且通过抛出 InterruptedException 来唤醒它;
如果线程遇到了IO 阻塞,无能为力,因为 IO 是操作系统实现的,Java 代码并没有办法直接
接触到操作系统

3.现在有 T1、T2、T3 三个线程,你怎样保证 T2 在 T1 执行完后执行,T3 在 T2 执行完后执行?

利用Thread类的join 方法

4.join方法传参和不传参的区别?

join方法中如果传入参数,则表示:如果A线程中调用B线程的join(10),则表示A线程会等待B线程执行10毫秒,10毫秒过后,A、B线程并行执行。

join(0)的意思不是A线程等待B线程0秒,而是A线程等待B线程无限时间,直到B线程执行完毕,即join(0)等价于join()。

5.join方法实现原理

join方法是通过调用线程的wait方法来达到同步的目的的。例如A线程中调用了B线程的join方法,则相当于在A线程中调用了B线程的wait方法,
当B线程执行完(或者到达等待时间),B线程会自动调用自身的notifyAll方法唤醒A线程,从而达到同步的目的。


由join方法源码可以看到:
1、如果join方法传参为0的话,则会调用isAlive()方法,一直检测线程是否存活(执行完毕),如果存活就调用wait方法,一直阻塞。
2、如果参数为负数,则直接报错:“timeout value is negative”
3、如果参数大于0,则while里面一直判断线程是否存活,存活的话就一直判断当前线程执行的时间并与计算还需要等待多久时间,最后如果等待时间小于等于0就跳出循环,否则就继续wait。

public final synchronized void join(long millis) throws InterruptedException {
        long base = System.currentTimeMillis();
        long now = 0;
        if (millis < 0) {
            throw new IllegalArgumentException("timeout value is negative");
        }
 
        if (millis == 0) {
            while (isAlive()) {
                wait(0);
            }
        } else {
            while (isAlive()) {
                long delay = millis - now;
                if (delay <= 0) {
                    break;
                }
                wait(delay);
                now = System.currentTimeMillis() - base;
            }
        }
    }

6.AQS 框 架 是 怎 么 回 事 儿 ?

AQS 全称为 AbstractQueuedSychronizer,翻译过来应该是抽象队列同步器。

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

  2. AQS 通 过 Node 内 部 类 构 成 的 一 个 双 向 链 表 结 构 的 同 步 队 列 , 来 完 成 线程 获 取 锁 的 排 队 工 作 , 当 有 线 程 获 取 锁 失 败 后 , 就 被 添 加 到 队 列 末 尾 。Node 类 是 对 要 访 问 同 步 代 码 的 线 程 的 封 装 , 包 含 了 线 程 本 身 及 其 状 态 叫waitStatus( 有 五 种 不 同 取 值 , 分 别 表 示 是 否 被 阻 塞 , 是 否 等 待 唤 醒 , 是否 已 经 被 取 消 等 ) , 每 个 Node 结 点 关 联 其 prev 结点和 next 结点,方 便 线 程 释 放 锁 后 快 速 唤 醒 下 一 个 在 等 待 的 线 程 , 是 一 个 FIFO 的过程。

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

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

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

7. Synchronized 和 ReentrantLock 的区别?

主要相同点:Lock 能完成 synchronized 所实现的所有功能。
主要不同点:Lock 有比 synchronized 更精确的线程语义和更好的性能。
synchronized 会自动释放锁,而 Lock 一定要求程序员手工释放,并且必须在 finally 从句中释放。Lock 还有更强大的功能,例如,它的 tryLock 方法可以非阻塞方式去拿锁。

8.创 建 线 程 池 的 几 个 核 心 构 造 参 数 ?

corePoolSize:线 程 池 的 核 心 线 程 数 。
maximumPoolSize:线 程 池 允 许 的 最 大 线 程 数 。
keepAliveTime:超 过 核 心 线 程 数 时 闲 置 线 程 的 存 活 时 间 。
workQueue:任 务 执 行 前 保 存 任 务 的 队 列 , 保 存 由 execute 方法提交的 Runnable 任务。

9.线程 池 中 的 线 程 是 怎 么 创 建 的 ?是 一 开 始 就 随 着 线 程 池 的 启 动 创建 好 的 吗 ?

显 然 不 是 的 。线 程 池 默 认 初 始 化 后 不 启 动 Worker, 等 待 有 请 求 时 才 启动 。每 当 我 们 调 用 execute() 方 法 添 加 一 个 任 务 时 , 线 程 池 会 做 如 下 判 断 :如 果 正 在 运 行 的 线 程 数 量 小 于 corePoolSize,那 么 马 上 创 建 线 程 运 行 这个任务;如 果 正 在 运 行 的 线 程 数 量 大 于 或 等 于 corePoolSize,那 么 将 这 个 任 务 放入队列;如果这时候队列满了,而且正在运行的线程数量小于maximumPoolSize, 那 么 还 是 要 创 建 非 核 心 线 程 立 刻 运 行 这 个 任 务 ;如果队列满了,而且正在运行的线程数量大于或等于maximumPoolSize , 那 么 线 程 池 会 抛 出 异 常RejectExecutionException。当 一 个 线 程 完 成 任 务 时 , 它 会 从 队 列 中 取 下 一 个 任 务 来 执 行 。当一个线程 无 事 可 做 , 超 过 一 定 的 时 间 ( keepAliveTime) 时 , 线 程 池 会 判 断 。如 果 当 前 运 行 的 线 程 数 大 于 corePoolSize,那 么 这 个 线 程 就 被 停 掉 。所以 线 程 池 的 所 有 任 务 完 成 后 , 它 最 终 会 收 缩 到 corePoolSize 的大小。

10.Java 中 默 认 实 现 好 的 线 程 池 又 有 哪 些 呢 ?

  1. SingleThreadExecutor 线程池 这 个 线 程 池 只 有 一 个 核 心 线 程 在 工 作 ,也 就 是 相 当 于 单 线 程 串 行 执 行 所 有任 务 。如 果 这 个 唯 一 的 线 程 因 为 异 常 结 束 ,那 么 会 有 一 个 新 的 线 程 来 替 代它 。此 线 程 池 保 证 所 有 任 务 的 执 行 顺 序 按 照 任 务 的 提 交 顺 序 执 行 。

  2. FixedThreadPool 线程池

FixedThreadPool 是 固 定 大 小 的 线 程 池 , 只 有 核 心 线 程 。每 次 提 交 一 个任 务 就 创 建 一 个 线 程 ,直 到 线 程 达 到 线 程 池 的 最 大 大 小 。线 程 池 的 大 小 一旦 达 到 最 大 值 就 会 保 持 不 变 ,如 果 某 个 线 程 因 为 执 行 异 常 而 结 束 ,那 么 线程 池 会 补 充 一 个 新 线 程 。

FixedThreadPool 多 数 针 对 一 些 很 稳 定 很 固 定 的 正 规 并 发 线 程 , 多 用 于服务器。

  1. CachedThreadPool 线程池

CachedThreadPool 是 无 界 线 程 池 , 如 果 线 程 池 的 大 小 超 过 了 处 理 任 务所 需 要 的 线 程 ,那 么 就 会 回 收 部 分 空 闲( 60 秒 不 执 行 任 务 )线 程 ,当 任务 数 增 加 时 , 此 线 程 池 又 可 以 智 能 的 添 加 新 线 程 来 处 理 任 务 。线 程 池 大 小 完 全 依 赖 于 操 作 系 统 ( 或 者 说 JVM) 能 够 创 建 的 最 大 线 程 大小 。SynchronousQueue 是 一 个 是 缓 冲 区 为 1 的 阻 塞 队 列 。

缓 存 型 池 子 通 常 用 于 执 行 一 些 生 存 期 很 短 的 异 步 型 任 务 ,因 此 在 一 些 面 向连接的 daemon 型 SERVER 中 用 得 不 多 。但 对 于 生 存 期 短 的 异 步 任 务 ,它 是 Executor 的首选。

  1. ScheduledThreadPool 线程池ScheduledThreadPool:核 心 线 程 池 固 定 , 大 小 无 限 的 线 程 池 。此 线 程池 支 持 定 时 以 及 周 期 性 执 行 任 务 的 需 求 。创 建 一 个 周 期 性 执 行 任 务 的 线 程池 。如 果 闲 置 ,非 核 心 线 程 池 会 在 DEFAULT_KEEPALIVEMILLIS 时间内回收。

11.如何在 Java 线 程 池 中 提 交 线 程 ?

线 程 池 最 常 用 的 提 交 任 务 的 方 法 有 两 种 :

  1. execute():ExecutorService.execute方法接收一个Runable 实例,它用来执行一个任务 :
  2. submit():ExecutorService.submit() 方法返回的是Future对象。 可以用 isDone() 来 查 询 Future 是 否 已 经 完 成 , 当 任 务 完 成 时 , 它 具有 一 个 结 果 ,可 以 调 用 get() 来 获 取 结 果 。也 可 以 不 用 isDone() 进 行检 查 就 直 接 调 用 get(),在 这 种 情 况 下 ,get() 将 阻 塞 ,直 至 结 果 准 备 就绪 。

12.请 谈 谈 volatile 有 什 么 特 点 ,为 什 么 它 能 保 证 变 量 对 所 有 线 程的 可 见 性 ?

关键字 volatile 是 Java 虚 拟 机 提 供 的 最 轻 量 级 的 同 步 机 制 。当 一 个 变量 被 定 义 成 volatile 之 后 , 具 备 两 种 特 性 :

  1. 保 证 此 变 量 对 所 有 线 程 的 可 见 性 。当 一 条 线 程 修 改 了 这 个 变 量 的 值 ,新 值对 于 其 他 线 程 是 可 以 立 即 得 知 的 。而 普 通 变 量 做 不 到 这 一 点 。

  2. 禁 止 指 令 重 排 序 优 化 。普 通 变 量 仅 仅 能 保 证 在 该 方 法 执 行 过 程 中 ,得 到 正确 结 果 , 但 是 不 保 证 程 序 代 码 的 执 行 顺 序 。

既 然 volatile 能 够 保 证 线 程 间 的 变 量 可 见 性 ,是 不 是 就 意 味 着 基 于 volatile 变 量 的 运 算 就 是 并 发 安 全 的 ?

显 然 不 是 的 。基 于 volatile 变 量 的 运 算 在 并 发 下 不 一 定 是 安 全 的 。volatile 变 量 在 各 个 线 程 的 工 作 内 存 , 不 存 在 一 致 性 问 题 ( 各 个 线 程 的工 作 内 存 中 volatile 变 量 , 每 次 使 用 前 都 要 刷 新 到 主 内 存 ) 。但 是 Java 里 面 的 运 算 并 非 原 子 操 作 ,导 致 volatile 变 量 的 运 算 在 并 发下 一 样 是 不 安 全 的 。

13.请谈谈 ThreadLocal 是 怎 么 解 决 并 发 安 全 的 ?

ThreadLocal 这 是 Java 提 供 的 一 种 保 存 线 程 私 有 信 息 的 机 制 , 因 为 其在 整 个 线 程 生 命 周 期 内 有 效 ,所 以 可 以 方 便 地 在 一 个 线 程 关 联 的 不 同 业 务模 块 之 间 传 递 信 息 , 比 如 事 务 ID、 Cookie 等 上 下 文 相 关 信 息 。ThreadLocal 为 每 一 个 线 程 维 护 变 量 的 副 本 , 把 共 享 数 据 的 可 见 范 围 限制 在 同 一 个 线 程 之 内 , 其 实 现 原 理 是 , 在 ThreadLocal 类 中 有 一 个Map, 用 于 存 储 每 一 个 线 程 的 变 量 的 副 本 。

14.请说出你所知道的线程同步的方法。

wait():使一个线程处于等待状态,并且释放所持有的对象的 lock。 
sleep():使一个正在运行的线程处于睡眠状态,是一个静态方法,调用此方法要捕捉InterruptedException 异常。 notify():唤醒一个处于等待状态的线程,注意的是在调用此方法的时候,并不能确切的唤醒某一个等待状态的线程,而是由 JVM 确定唤醒哪个线程,而且不是按优先级。 notityAll():唤醒所有处入等待状态的线程,注意并不是给所有唤醒线程一个对象的锁,而是让它们竞争。

15.ReadWriteLock 是什么?

首先明确一下,不是说 ReentrantLock 不好,只是 ReentrantLock 某些时 候有局限。如果使用 ReentrantLock,可能本身是为了防止线程 A 在写数据、线程 B 在读数据造成的数据不一致,但这样,如果线程 C 在读数据、线程 D也在读数据,读数据是不会改变数据的,没有必要加锁,但是还是加锁了,降低了程序的性能。因为这个,才诞生了读写锁 ReadWriteLock。ReadWriteLock 是一个读写锁接口,ReentrantReadWriteLock 是 ReadWriteLock 接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。

16.什么是 CAS

CAS,全称为 Compare and Swap,即比较-替换。假设有三个操作数:内
存值 V、旧的预期值 A、要修改的值 B,当且仅当预期值 A 和内存值 V 相同
时,才会将内存值修改为 B 并返回 true,否则什么都不做并返回 false。当
然 CAS 一定要 volatile 变量配合,这样才能保证每次拿到的变量是主内存中
最新的那个值,否则旧的预期值 A 对某条线程来说,永远是一个不会变的值 A,只要某次 CAS 操作失败,永远都不可能成功。

如有错漏之处,敬请指正