【多线程】高频面试题合集

时间:2022-03-10 01:11:57

【多线程】高频面试题合集

✨个人主页:bit me????
✨当前专栏:Java EE初阶????

目 录


  1. 你是怎么理解乐观锁悲观锁的,具体怎么实现呢?

悲观锁认为多个线程访问同一个共享变量冲突的概率较大, 会在每次访问共享变量之前都去真正加锁.
 
乐观锁认为多个线程访问同一个共享变量冲突的概率不大. 并不会真的加锁, 而是直接尝试访问数据. 在访问的同时识别当前的数据是否出现访问冲突.
 
悲观锁的实现就是先加锁(比如借助操作系统提供的 mutex), 获取到锁再操作数据. 获取不到锁就等待.
 
乐观锁的实现可以引入一个版本号. 借助版本号识别出当前的数据访问是否冲突.

  1. 介绍下读写锁?

读写锁就是把读操作和写操作分别进行加锁.
 
读锁和读锁之间不互斥.
 
写锁和写锁之间互斥.
 
写锁和读锁之间互斥.
 
读写锁最主要用在 “频繁读, 不频繁写” 的场景中.

  1. 什么是自旋锁,为什么要使用自旋锁策略呢,缺点是什么?

如果获取锁失败, 立即再尝试获取锁, 无限循环, 直到获取到锁为止. 第一次获取锁失败, 第二次的尝试会在极短的时间内到来. 一旦锁被其他线程释放, 就能第一时间获取到锁.
 
相比于挂起等待锁,
 
优点: 没有放弃 CPU 资源, 一旦锁被释放就能第一时间获取到锁, 更高效. 在锁持有时间比较短的场景下非常有用.
 
缺点: 如果锁的持有时间较长, 就会浪费 CPU 资源

  1. synchronized可重入锁么?

是可重入锁.
 
可重入锁指的就是连续两次加锁不会导致死锁.
 
实现的方式是在锁中记录该锁持有的线程身份, 以及一个计数器(记录加锁次数). 如果发现当前加锁的线程就是持有锁的线程, 则直接计数自增.

  1. 讲解下你自己理解的 CAS 机制

全称 Compare and swap, 即 “比较并交换”. 相当于通过一个原子的操作, 同时完成 “读取内存, 比较是否相等, 修改内存” 这三个步骤. 本质上需要 CPU 指令的支撑.

  1. ABA问题怎么解决?

给要修改的数据引入版本号. 在 CAS 比较数据当前值和旧值的同时, 也要比较版本号是否符合预期.
 
如果发现当前版本号和之前读到的版本号一致, 就真正执行修改操作, 并让版本号自增; 如果发现当前版本号比之前读到的版本号大, 就认为操作失败

  1. 什么是偏向锁?

偏向锁不是真的加锁, 而只是在锁的对象头中记录一个标记(记录该锁所属的线程). 如果没有其他线程参与竞争锁, 那么就不会真正执行加锁操作, 从而降低程序开销. 一旦真的涉及到其他的线程竞争, 再取消偏向锁状态, 进入轻量级锁状态.

  1. synchronized 实现原理 是什么?

参考 ????【多线程】synchronized 原理 这篇文章????

  1. 介绍下 Callable 是什么

Callable 是一个 interface . 相当于把线程封装了一个 “返回值”. 方便程序猿借助多线程的方式计算结果.
 
Callable 和 Runnable 相对, 都是描述一个 “任务”. Callable 描述的是带有返回值的任务,
Runnable 描述的是不带返回值的任务.
 
Callable 通常需要搭配 FutureTask 来使用. FutureTask 用来保存 Callable 的返回结果. 因为 Callable 往往是在另一个线程中执行的, 啥时候执行完并不确定.
 
FutureTask 就可以负责这个等待结果出来的工作.

  1. 线程同步的方式有哪些?

synchronized, ReentrantLock, Semaphore 等都可以用于线程同步

  1. 为什么有了 synchronized 还需要 juc 下的 lock

以 juc 的 ReentrantLock 为例:

  • synchronized 使用时不需要手动释放锁. ReentrantLock 使用时需要手动释放. 使用起来更灵活,
  • synchronized 在申请锁失败时, 会死等. ReentrantLock 可以通过 trylock 的方式等待一段时间就放弃.
  • synchronized 是非公平锁, ReentrantLock 默认是非公平锁. 可以通过构造方法传入一个 true 开启公平锁模式.
  • synchronized 是通过 Object 的 wait / notify 实现等待-唤醒. 每次唤醒的是一个随机等待的线程. ReentrantLock 搭配 Condition 类实现等待-唤醒, 可以更精确控制唤醒某个指定的线程.
  1. AtomicInteger 的实现原理是什么?

基于 CAS 机制. 伪代码如下:

class AtomicInteger {
	private int value;
	public int getAndIncrement() {
     	int oldValue = value;
     	while ( CAS(value, oldValue, oldValue+1) != true) {
         	oldValue = value;
     	}
     	return oldValue;
 	}
}

执行过程参考我的文章????【多线程】CAS ???? 中的"CAS 典型应用场景"

  1. 信号量听说过么?之前都用在过哪些场景下?

信号量, 用来表示 “可用资源的个数”. 本质上就是一个计数器.
 
使用信号量可以实现 “共享锁”, 比如某个资源允许 3 个线程同时使用, 那么就可以使用 P 操作作为加锁, V 操作作为解锁, 前三个线程的 P 操作都能顺利返回, 后续线程再进行 P 操作就会阻塞等待, 直到前面的线程执行了 V 操作.

  1. 解释一下 ThreadPoolExecutor 构造方法的参数的含义

参考我的文章????【多线程】JUC中的线程池 ????

  1. ConcurrentHashMap 的读是否要加锁,为什么?

读操作没有加锁. 目的是为了进一步降低锁冲突的概率. 为了保证读到刚修改的数据, 搭配了 volatile 关键字.

  1. 介绍下 ConcurrentHashMap 的锁分段技术?

这个是 Java1.7 中采取的技术. Java1.8 中已经不再使用了. 简单的说就是把若干个哈希桶分成一个 “段” (Segment), 针对每个段分别加锁.
 
目的也是为了降低锁竞争的概率. 当两个线程访问的数据恰好在同一个段上的时候, 才触发锁竞争.

  1. ConcurrentHashMap 在jdk1.8做了哪些优化?

取消了分段锁, 直接给每个哈希桶(每个链表)分配了一个锁(就是以每个链表的头结点对象作为锁对象).
 
将原来 数组 + 链表 的实现方式改进成 数组 + 链表 / 红黑树 的方式. 当链表较长的时候(大于等于8 个元素)就转换成红黑树.

  1. HashtableHashMapConcurrentHashMap 之间的区别?
  1. HashMap 线程不安全的,Hashtable 和 ConcurrentHashMap 是线程安全的!
  2. Hashtable 是使用一把大锁,锁冲突的概率很高,ConcurrentHashMap 则是每个哈希桶一把锁,锁冲突概率大大降低了。
  3. 详细说 ConcurrentHashMap 其他的优化策略。???? 线程安全的集合类 与 死锁 ???? 中的 多线程环境使用哈希表
  4. HashMap key 允许为 null,另外两个不允许。
  1. 谈谈死锁是什么,如何避免死锁避免算法? 实际解决过没有?

参考我的文章:???? 线程安全的集合类 与 死锁 ???? 中的死锁问题

  1. 谈谈 volatile 关键字的用法?

volatile 能够保证内存可见性. 强制从主内存中读取数据. 此时如果有其他线程修改被 volatile 修饰的变量, 可以第一时间读取到最新的值.

  1. Java多线程是如何实现数据共享的?

JVM 把内存分成了这几个区域:
 
方法区, 堆区, 栈区, 程序计数器.
 
其中堆区这个内存区域是多个线程之间共享的.
 
只要把某个数据放到堆内存中, 就可以让多个线程都能访问到

  1. Java创建线程池的接口是什么?参数 LinkedBlockingQueue 的作用是什么?

创建线程池主要有两种方式:

  • 通过 Executors 工厂类创建. 创建方式比较简单, 但是定制能力有限.
  • 通过 ThreadPoolExecutor 创建. 创建方式比较复杂, 但是定制能力强.

 
LinkedBlockingQueue 表示线程池的任务队列. 用户通过 submit / execute 向这个任务队列中添加任务, 再由线程池中的工作线程来执行任务.

  1. Java线程共有几种状态?状态之间怎么切换的?
  • NEW: 安排了工作, 还未开始行动. 新创建的线程, 还没有调用 start 方法时处在这个状态.
  • RUNNABLE: 可工作的. 又可以分成正在工作中和即将开始工作. 调用 start 方法之后, 并正在CPU 上运行/在即将准备运行 的状态.
  • BLOCKED: 使用 synchronized 的时候, 如果锁被其他线程占用, 就会阻塞等待, 从而进入该状态.
  • WAITING: 调用 wait 方法会进入该状态.
  • TIMED_WAITING: 调用 sleep 方法或者 wait(超时时间) 会进入该状态.
  • TERMINATED: 工作完成了. 当线程 run 方法执行完毕后, 会处于这个状态.
  1. 在多线程下,如果对一个数进行叠加,该怎么做?
  • 使用 synchronized / ReentrantLock 加锁
  • 使用 AtomInteger 原子操作.
  1. Servlet 是否是线程安全的?

Servlet 本身是工作在多线程环境下.
 
如果在 Servlet 中创建了某个成员变量, 此时如果有多个请求到达服务器, 服务器就会多线程进行操作, 是可能出现线程不安全的情况的.

  1. ThreadRunnable 的区别和联系?

Thread 类描述了一个线程.
 
Runnable 描述了一个任务.
 
在创建线程的时候需要指定线程完成的任务, 可以直接重写 Thread 的 run 方法, 也可以使用 Runnable 来描述这个任务.

  1. 多次 start 一个线程会怎么样

第一次调用 start 可以成功调用.
 
后续再调用 start 会抛出 java.lang.IllegalThreadStateException 异常

  1. synchronized 两个方法,两个线程分别同时用这个方法,请问会发生什么?

synchronized 加在非静态方法上, 相当于针对当前对象加锁.
 
如果这两个方法属于同一个实例:
 
线程1 能够获取到锁, 并执行方法. 线程2 会阻塞等待, 直到线程1 执行完毕, 释放锁, 线程2 获取到锁之后才能执行方法内容.
 
如果这两个方法属于不同实例:
 
两者能并发执行, 互不干扰

  1. 进程线程的区别?
  • 进程是包含线程的. 每个进程至少有一个线程存在,即主线程。
  • 进程和进程之间不共享内存空间. 同一个进程的线程之间共享同一个内存空间.
  • 进程是系统分配资源的最小单位,线程是系统调度的最小单位。