【多线程】常见面试题

简介: 多线程面试题总结

 1.你知道线程与进程的区别吗?

    1. 进程是系统进行资源分配和调度的一个独立单位,线程是程序执行的最小单位,一个进程必然有一条线程(主线程)。
    2. 进程有自己的内存地址空间,线程只独享指令流执行的必要资源,如寄存器和栈。
    3. 由于同一进程的各线程间共享内存和文件资源,可以不通过内核进行直接通信。
    4. 线程的创建、切换及终止效率更高。
    5. 进程之间互不影响,线程之间可以相互影响。

    2.你知道创建线程有几种方式吗?

      1. 继承Thread类,并实现run()方法。
      2. 实现Runnable接口,并实现run()方法。
      3. 实现Callable接口,并实现call()方法。
      4. 通过线程池来自动创建线程。

      3.Runable与Callable的区别?

        1. 两个都是描述一个“任务”,实现Callable接口的任务线程能返回执行结果,此时需要调用FutureTask.get()方法实现,此方法会阻塞主线程直到获取call()返回的结果;当不调用此方法时,主线程不会阻塞;而实现Runnable接口的任务线程不能返回结果。
        2. Callable实现的是call()方法,Runnable实现的是run()方法。
        3. Callable接口的call()方法允许抛出异常;而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛。

        4.线程池有几种?

        JDK提供的创建线程池的方法有六种:

        // 1. 用来处理大量短时间工作任务的线程池,如果池中没有可用的线程将创建新的线程,如果线程空闲60秒将收回并移出缓存
        ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
        // 2. 创建一个操作无界队列且固定大小线程池
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(3);
        // 3. 创建一个操作无界队列且只有一个工作线程的线程池
        ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
        // 4. 创建一个单线程执行器,可以在给定时间后执行或定期执行。
        ScheduledExecutorService singleThreadScheduledExecutor = Executors.newSingleThreadScheduledExecutor();
        // 5. 创建一个指定大小的线程池,可以在给定时间后执行或定期执行。
        ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
        // 6. 创建一个指定大小(不传入参数,为当前机器CPU核心数)的线程池,并行地处理任务,不保证处理顺序
        Executors.newWorkStealingPool();

        image.gif

        5.手动创建线程池的时候ThreadPoolExcutor有多个参数,以及参数的含义?

        image.gif编辑

          • int corePoolSize:核心线程数,创建线程池包含的最小线程数量。
          • int maximumPoolSize:最大线程数,也可以叫做临时线程数。
          • long keepAliveTime:临时线程空闲时长。
          • TimeUnit unit:空闲时间单位,和keepAliveTime一起使用。
          • BlockingQueue<Runnable> workQueue:用来保存任务的阻塞队列。
          • RejectedExecutionHandler handler:拒绝策略,触发的时机:当线程池处理不了过多的任务时。

          6.线程池中的拒绝策略都有哪些?

          image.gif编辑

          7.线程池的工作流程是什么?

          image.gif编辑

            1. 用核心线程去执行任务;
            2. 多于任务会被保存到阻塞队列中;
            3. 当阻塞队列满了之后,就会创建临时线程;
            4. 当阻塞队列满了而且临时线程也创建完成,之后再提交的线程就会执行拒绝策略;
            5. 当任务被成功处理之后,临时线程经过一段空闲时间将会被系统收回。

            8.知道线程安全问题吗?

            如果多线程环境下代码运行的结果是不符合我们预期的,即不是在单线程环境应该的结果,则说这个程序是线程不安全的。

            可能造成线程不安全原因:

              1. 多个线程修改了同一个变量;
              2. 线程是抢占式执行的,CPU调度是随机的;
              3. 没有保证原子性;
              4. 没有保证内存可见性;
              5. 没有保证有序性。

              9.怎么解决线程安全问题?

              保证线程安全的思路:

              1. 使用没有共享资源的模型

              2. 适用共享资源只读,不写的模型

                     1. 不需要写共享资源的模型

                     2. 使用不可变对象

              3. 直面线程安全(重点)

                     1. 保证原子性

                     2. 保证顺序性

                     3. 保证可见性

              具体解决可见之前的博客总结:线程安全问题及解决方法

              10.synchronized和volatile的作用与区别?

              synchronized特性:

                1. 互斥性:同一时间只能有一个线程获取到锁,其他的线程就要阻塞等待;
                2. 刷新内存:线程修改完变量的值后,会把新值写回到主内存,将并行改为串行方式来做;
                3. 可重入:一个线程可以对同一个锁对象加多次锁;

                volatile特性:

                  1. 可以保证内存可见性
                  2. 可以解决有序性
                  3. 不具备原子性

                  synchronized和volatile的区别:

                    1. volatile仅能使用在变量级别,synchronized则可以使用在变量、方法、类级别上。
                    2. volatile仅仅能实现变量修改可见性,并不能保证原子性,synchronized可以实现变量的修改可见性和原子性。
                    3. volatile不会造成线程阻塞,synchronized可能会造成线程阻塞。
                    4. volatile标记的变量不会被编译器优化,synchronized标记的变量可以被编译器优化。

                    11.JMM特征?

                    JMM 是Java内存模型( Java Memory Model),简称JMM。它本身只是一个抽象的概念,并不真实存在,它描述的是一种规则或规范,是和多线程相关的一组规范。

                    image.gif编辑

                    JMM 抽象出主存储器(Main Memory)和工作存储器(Working Memory)两种。

                    ·主存储器是实例对象所在的区域,所有的实例都存在于主存储器内。比如,实例所拥有的字段即位于主存储器内,主存储器是所有的线程所共享的。

                    ·工作存储器是线程所拥有的作业区,每个线程都有其专用的工作存储器。工作存储器存有主存储器中必要部分的拷贝,称之为工作拷贝(Working Copy)。

                    所以,线程无法直接对主内存进行操作,此外,线程A想要和线程B通信,只能通过主存进行。

                    JMM的三大特性:原子性、可见性、有序性。

                    1.原子性

                    一个或多个操作,要么全部执行,要么全部不执行(执行的过程中是不会被任何因素打断的)。

                    2.可见性

                    只要有一个线程对共享变量的值做了修改,其他线程都将马上收到通知,立即获得最新值。

                    3.有序性

                    有序性可以总结为:在本线程内观察,所有的操作都是有序的;而在一个线程内观察另一个线程,所有操作都是无序的。前半句指 as-if-serial 语义:线程内似表现为串行,后半句是指:“指令重排序现象”和“工作内存与主内存同步延迟现象”。处理器为了提高程序的运行效率,提高并行效率,可能会对代码进行优化。编译器认为,重排序后的代码执行效率更优。这样一来,代码的执行顺序就未必是编写代码时候的顺序了,在多线程的情况下就可能会出错。

                    在代码顺序结构中,我们可以直观的指定代码的执行顺序, 即从上到下按序执行。但编译器和CPU处理器会根据自己的决策,对代码的执行顺序进行重新排序,优化指令的执行顺序,提升程序的性能和执行速度,使语句执行顺序发生改变,出现重排序,但最终结果看起来没什么变化(在单线程情况下)。

                    有序性问题 指的是在多线程的环境下,由于执行语句重排序后,重排序的这一部分没有一起执行完,就切换到了其它线程,导致计算结果与预期不符的问题。这就是编译器的编译优化给并发编程带来的程序有序性问题。

                    Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性,volatile 是因为其本身包含“禁止指令重排序”的语义,synchronized 是由“一个变量在同一个时刻只允许一条线程对其进行 lock 操作”这条规则获得的,此规则决定了持有同一个对象锁的两个同步块只能串行进入。

                    12.synchronized的锁升级过程?

                    Synchronized 具有以下特性(只考虑 JDK 1.8):

                      1. 开始时是乐观锁, 如果锁冲突频繁, 就转换为悲观锁.
                      2. 开始是轻量级锁实现, 如果锁被持有的时间较长, 就转换成重量级锁.
                      3. 实现轻量级锁的时候大概率用到的自旋锁策略
                      4. 是一种不公平锁
                      5. 是一种可重入锁
                      6. 不是读写锁

                      image.gif编辑

                      13.什么是偏向锁、轻量级锁、重量级锁?

                      1) 偏向锁

                      第一个尝试加锁的线程, 优先进入偏向锁状态.

                      偏向锁不是真的 "加锁", 只是给对象头中做一个 "偏向锁的标记", 记录这个锁属于哪个线程.

                      如果后续没有其他线程来竞争该锁, 那么就不用进行其他同步操作了(避免了加锁解锁的开销)

                      如果后续有其他线程来竞争该锁(刚才已经在锁对象中记录了当前锁属于哪个线程了, 很容易识别 当前申请锁的线程是不是之前记录的线程), 那就取消原来的偏向锁状态, 进入一般的轻量级锁状态.

                      偏向锁本质上相当于 "延迟加锁" . 能不加锁就不加锁, 尽量来避免不必要的加锁开销.

                      但是该做的标记还是得做的, 否则无法区分何时需要真正加锁.

                      2) 轻量级锁

                      随着其他线程进入竞争, 偏向锁状态被消除, 进入轻量级锁状态(自适应的自旋锁).

                      此处的轻量级锁就是通过 CAS 来实现.

                        • 通过 CAS 检查并更新一块内存 (比如 null => 该线程引用)
                        • 如果更新成功, 则认为加锁成功
                        • 如果更新失败, 则认为锁被占用, 继续自旋式的等待(并不放弃 CPU).

                        自旋操作是一直让 CPU 空转, 比较浪费 CPU 资源.

                        因此此处的自旋不会一直持续进行, 而是达到一定的时间/重试次数, 就不再自旋了.

                        也就是所谓的 "自适应"

                        3) 重量级锁

                        如果竞争进一步激烈, 自旋不能快速获取到锁状态, 就会膨胀为重量级锁

                        此处的重量级锁就是指用到内核提供的 mutex .

                          • 执行加锁操作, 先进入内核态.
                          • 在内核态判定当前锁是否已经被占用
                          • 如果该锁没有占用, 则加锁成功, 并切换回用户态.
                          • 如果该锁被占用, 则加锁失败. 此时线程进入锁的等待队列, 挂起. 等待被操作系统唤醒.
                          • 经历了一系列的沧海桑田, 这个锁被其他线程释放了, 操作系统也想起了这个挂起的线程, 于是唤醒 这个线程, 尝试重新获取锁.

                          14.介绍一下CAS,以及ABA问题?

                          CAS: 全称Compare and swap,字面意思:”比较并交换“,一个 CAS 涉及到以下操作:

                          我们假设内存中的原数据V,旧的预期值A,需要修改的新值B。

                            1. 比较 A 与 V 是否相等。(比较)
                            2. 如果比较相等,将 B 写入 V。(交换)
                            3. 返回操作是否成功。

                            CAS 伪代码

                            boolean CAS(address, expectValue, swapValue) {
                             if (&address == expectedValue) {
                               &address = swapValue;
                                    return true;
                               }
                                return false;
                            }

                            image.gif

                            当多个线程同时对某个资源进行CAS操作,只能有一个线程操作成功,但是并不会阻塞其他线程,其他线 程只会收到操作失败的信号。

                            CAS 可以视为是一种乐观锁. (或者可以理解成 CAS 是乐观锁的一种实现方式

                            ABA 的问题:

                            假设存在两个线程 t1 和 t2. 有一个共享变量 num, 初始值为 A.

                            接下来, 线程 t1 想使用 CAS 把 num 值改成 Z, 那么就需要

                              • 先读取 num 的值, 记录到 oldNum 变量中.
                              • 使用 CAS 判定当前 num 的值是否为 A, 如果为 A, 就修改成 Z.

                              但是, 在 t1 执行这两个操作之间, t2 线程可能把 num 的值从 A 改成了 B, 又从 B 改成了 A

                              解决方案

                              给要修改的值, 引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期. CAS 操作在读取旧值的同时, 也要读取版本号.

                              真正修改的时候,

                                • 如果当前版本号和读到的版本号相同, 则修改数据, 并把版本号 + 1.
                                • 如果当前版本号高于读到的版本号. 就操作失败(认为数据已经被修改过了).

                                15.ReentrantLock特性?以及和synchronized的区别?

                                ReentrantLock 也是可重入互斥锁. 和 synchronized 定位类似, 都是用来实现互斥效果, 保证线程安全.

                                ReentrantLock 的用法:

                                  • lock(): 加锁, 如果获取不到锁就死等.
                                  • trylock(超时时间): 加锁, 如果获取不到锁, 等待一定的时间之后就放弃加锁.
                                  • unlock(): 解锁

                                  ReentrantLock 和 synchronized 的区别:

                                    • synchronized 是一个关键字, 是 JVM 内部实现的(大概率是基于 C++ 实现). ReentrantLock 是标准 库的一个类, 在 JVM 外实现的(基于 Java 实现).
                                    • synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活, 但是也容易遗漏 unlock.
                                    • synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就 放弃.
                                    • synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个 true 开启 公平锁模式.
                                    • 更强大的唤醒机制. synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一 个随机等待的线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指 定的线程.

                                    16.JUC包下的工具类知道那些?

                                    信号量 Semaphore

                                    信号量, 用来表示 "可用资源的个数". 本质上就是一个计数器.

                                    理解信号量

                                    可以把信号量想象成是停车场的展示牌: 当前有车位 100 个. 表示有 100 个可用资源.

                                    当有车开进去的时候, 就相当于申请一个可用资源, 可用车位就 -1 (这个称为信号量的 P 操作)

                                    当有车开出来的时候, 就相当于释放一个可用资源, 可用车位就 +1 (这个称为信号量的 V 操作)

                                    如果计数器的值已经为 0 了, 还尝试申请资源, 就会阻塞等待, 直到有其他线程释放资源.

                                    Semaphore 的 PV 操作中的加减计数器操作都是原子的, 可以在多线程环境下直接使用.

                                    代码示例

                                      • 创建 Semaphore 示例, 初始化为 4, 表示有 4 个可用资源.
                                      • acquire 方法表示申请资源(P操作), release 方法表示释放资源(V操作)
                                      • 创建 20 个线程, 每个线程都尝试申请资源, sleep 1秒之后, 释放资源. 观察程序的执行效果.
                                      Semaphore semaphore = new Semaphore(4);
                                      Runnable runnable = new Runnable() {
                                          @Override
                                          public void run() {
                                              try {
                                                  System.out.println("申请资源");
                                                  semaphore.acquire();
                                                  System.out.println("我获取到资源了");
                                                  Thread.sleep(1000);
                                                  System.out.println("我释放资源了");
                                                  semaphore.release();
                                             } catch (InterruptedException e) {
                                                  e.printStackTrace();
                                             }
                                         }
                                      };
                                      for (int i = 0; i < 20; i++) {
                                          Thread t = new Thread(runnable);
                                          t.start();
                                      }

                                      image.gif

                                      CountDownLatch

                                      同时等待 N 个任务执行结束.

                                      好像跑步比赛,10个选手依次就位,哨声响才同时出发;所有选手都通过终点,才能公布成绩。

                                        • 构造 CountDownLatch 实例, 初始化 10 表示有 10 个任务需要完成.
                                        • 每个任务执行完毕, 都调用 latch.countDown() . 在 CountDownLatch 内部的计数器同时自减.
                                        • 主线程中使用 latch.await(); 阻塞等待所有任务执行完毕. 相当于计数器为 0 了.
                                        public class Demo {
                                            public static void main(String[] args) throws Exception {
                                                CountDownLatch latch = new CountDownLatch(10);
                                                Runnable r = new Runable() {
                                                    @Override
                                                    public void run() {
                                                        try {
                                                            Thread.sleep(Math.random() * 10000);
                                                            latch.countDown();
                                                       } catch (Exception e) {
                                                            e.printStackTrace();
                                                       }
                                                   }
                                               };
                                                for (int i = 0; i < 10; i++) {
                                                    new Thread(r).start();
                                               }
                                           // 必须等到 10 人全部回来
                                                latch.await();
                                                System.out.println("比赛结束");
                                            }
                                        }

                                        image.gif

                                        17.线程安全集合类都有哪些?

                                        Java 标准库中很多都是线程不安全的. 这些类可能会涉及到多线程修改共享数据, 又没有任何加锁措施.

                                          • ArrayList
                                          • LinkedList
                                          • HashMap
                                          • TreeMap
                                          • HashSet
                                          • TreeSet
                                          • StringBuilder

                                          但是还有一些是线程安全的. 使用了一些锁机制来控制.

                                            • Vector (不推荐使用)
                                            • HashTable (不推荐使用)
                                            • ConcurrentHashMap
                                            • StringBuffer

                                            还有的虽然没有加锁, 但是不涉及 "修改", 仍然是线程安全的

                                              • String

                                              线程安全集合类可以分为三大类 :

                                                • ① 遗留的安全集合如Hashtable,Vector。
                                                • ② 使用Collections装饰的线程安全类如Collections.synchronizedList、Collections.synchronizedMap等。
                                                • ③ JUC包下的线程安全集合如Blocking类、CopyOnWrite类、Concurrent类等。

                                                ①是直接在方法上加上了synchronized锁,②是方法内部加上了synchronized锁。

                                                18.用过ConcurrentHashMap吗?介绍一下?

                                                相比于 Hashtable 做出了一系列的改进和优化. 以 Java1.8 为例

                                                读操作没有加锁(但是使用了 volatile 保证从内存读取结果), 只对写操作进行加锁. 加锁的方式仍然 是是用 synchronized, 但是不是锁整个对象, 而是 "锁桶" (用每个链表的头结点作为锁对象), 大大降 低了锁冲突的概率.

                                                充分利用 CAS 特性. 比如 size 属性通过 CAS 来更新. 避免出现重量级锁的情况.

                                                优化了扩容方式: 化整为零

                                                  • 发现需要扩容的线程, 只需要创建一个新的数组, 同时只搬几个元素过去.
                                                  • 扩容期间, 新老数组同时存在.
                                                  • 后续每个来操作 ConcurrentHashMap 的线程, 都会参与搬家的过程. 每个操作负责搬运一小 部分元素.
                                                  • 搬完最后一个元素再把老数组删掉.
                                                  • 这个期间, 插入只往新数组加.
                                                  • 这个期间, 查找需要同时查新数组和老数组

                                                  19.死锁问题?

                                                  死锁是这样一种情形:多个线程同时被阻塞,它们中的一个或者全部都在等待某个资源被释放。由于线程被无限期地阻塞,因此程序不可能正常终止。

                                                  死锁是一种严重的 BUG!! 导致一个程序的线程 "卡死", 无法正常工作!

                                                  如何避免死锁

                                                  死锁产生的四个必要条件:

                                                    • 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
                                                    • 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
                                                    • 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
                                                    • 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样 就形成了一个等待环路。

                                                    当上述四个条件都成立的时候,便形成死锁。当然,死锁的情况下如果打破上述任何一个条件,便可让死锁消失。

                                                    20.ThreadLocal了解不?

                                                    这个了解较少,了解一下其他博主的详细介绍ThreadLocal 详解

                                                    相关文章
                                                    |
                                                    29天前
                                                    |
                                                    Java 程序员
                                                    java线程池讲解面试
                                                    java线程池讲解面试
                                                    52 1
                                                    |
                                                    1月前
                                                    |
                                                    安全 Java
                                                    Qt经典面试题:Qt开启线程的几种方式
                                                    Qt经典面试题:Qt开启线程的几种方式
                                                    22 0
                                                    |
                                                    1月前
                                                    |
                                                    并行计算 安全 Java
                                                    C# .NET面试系列四:多线程
                                                    <h2>多线程 #### 1. 根据线程安全的相关知识,分析以下代码,当调用 test 方法时 i > 10 时是否会引起死锁? 并简要说明理由。 ```c# public void test(int i) { lock(this) { if (i > 10) { i--; test(i); } } } ``` 在给定的代码中,不会发生死锁。死锁通常是由于两个或多个线程互相等待对方释放锁而无法继续执行的情况。在这个代码中,只有一个线程持有锁,且没有其他线程参与,因此不
                                                    105 3
                                                    |
                                                    3月前
                                                    |
                                                    Java 调度 Windows
                                                    JAVA面试八股文之多线程基础知识
                                                    JAVA面试八股文之多线程基础知识
                                                    |
                                                    3月前
                                                    |
                                                    调度
                                                    【面试问题】说说线程的生命周期?
                                                    【1月更文挑战第27天】【面试问题】说说线程的生命周期?
                                                    |
                                                    3天前
                                                    |
                                                    Java 调度
                                                    Java面试必考题之线程的生命周期,结合源码,透彻讲解!
                                                    Java面试必考题之线程的生命周期,结合源码,透彻讲解!
                                                    31 1
                                                    |
                                                    10天前
                                                    |
                                                    调度 Python
                                                    Python多线程、多进程与协程面试题解析
                                                    【4月更文挑战第14天】Python并发编程涉及多线程、多进程和协程。面试中,对这些概念的理解和应用是评估候选人的重要标准。本文介绍了它们的基础知识、常见问题和应对策略。多线程在同一进程中并发执行,多进程通过进程间通信实现并发,协程则使用`asyncio`进行轻量级线程控制。面试常遇到的问题包括并发并行混淆、GIL影响多线程性能、进程间通信不当和协程异步IO理解不清。要掌握并发模型,需明确其适用场景,理解GIL、进程间通信和协程调度机制。
                                                    28 0
                                                    |
                                                    1月前
                                                    |
                                                    消息中间件 存储 算法
                                                    【C/C++ 泡沫精选面试题04】在实际项目中,多进程和多线程如何选择?
                                                    【C/C++ 泡沫精选面试题04】在实际项目中,多进程和多线程如何选择?
                                                    43 1
                                                    |
                                                    1月前
                                                    |
                                                    消息中间件 算法 开发者
                                                    【Qt面试题】多线程情况下, Qt中的信号槽分别在什么线程中执行, 如何控制?
                                                    【Qt面试题】多线程情况下, Qt中的信号槽分别在什么线程中执行, 如何控制?
                                                    20 1
                                                    |
                                                    1月前
                                                    |
                                                    Java 调度
                                                    金三银四面试必问:线程有几种状态
                                                    金三银四面试必问:线程有几种状态
                                                    14 0

                                                    热门文章

                                                    最新文章