Java多线程面试题

1.进程和线程的区别

  • 进程是正在运行程序的实例,进程中包含了线程,每个线程执行不同的任务;
  • 不同的进程使用了不同的内存空间,进程下的所有线程共享内存空间;
  • 线程更轻量,线程上下文切换一般要比进程上下文切换更低(上下文切换指的是一个线程切换为另外一个线程)

2.并发和并行

  • 并发是单核CPU上的多任务处理,多个任务在同一时间段内通过时间片轮转实现交替执行,用于解决IO密集型任务的瓶颈
  • 并行是多核CPU上的多任务处理,多个任务同一时间同时执行。

3.创建线程的方式?

  • 继承Thread类: 继承Thread类,并重写里面的run()方法,在run方法中定义线程具体任务。创建实例对象之后,调用start()方法启动线程
  • 实现Runnable接口: 实现Runnable接口要重写Run方法。并将实现类的对象作为参数传递给 Thread 对象的构造方法,最后调用 start() 方法启动线程。
  • 实现Callable接口和Future Task: 实现Callable接口,并重写call方法,然后创建 Future Task 对象,参数为 Callable 实现类的对象;紧接着创建 Thread 对象,参数为 Future Task 对象,最后调用 start() 方法启动线程。
  • 使用线程池: 通过Executors工具类创建线程池,然后将Runnable或Callable任务提交给线程池执行。
    • 在通常情况下我们会采用线程池的方式来创建线程,主要优化资源利用,提高性能和简化任务管理。

4.Java线程状态有哪些?

  • New: 线程被创建但未被启用
  • Runnable: 代表线程处于就绪或者正在运行状态,由操作系统调度
  • Blocked: 等待监视器锁时,陷入阻塞状态
  • Waiting: 等待状态,线程正在等待另一个线程执行特定的操作
  • Time Waiting:具有具体时间的等待状态
  • Terminated: 线程完成执行,终止状态

5.Sleep和Wait的区别

sleep会让当前线程处于休眠,不需要获取对象锁,属于Thread类的方法;

Wait会让获得对象处于线程等待,需要提前获得对象锁,属于Object类的方法。

6.怎么保证线程安全

  1. 可以使用Synchronized关键字给方法或者代码块加锁,线程在执行同步方法或者同步代码块时,会获取类锁或者对象锁,其它线程就会阻塞并等待锁
  2. 如果需要保证变量内存的可见性,可以使用Volatile关键字
  3. Lock接口和Reentrant Lock类:Lock提供了Reentrant Lock锁,它提供了更灵活的锁管理和更高的性能。
  4. 对于线程独立的数据,可以使用Thread Local来为每个线程提供专属的副本变量

7.如何保证线程的执行顺序

  • 用线程类的 **Join()**方法在一个线程中启动另外一个线程,另外一个线程完成该线程继续执行

8.notify和notify All的区别

  • **notify:**只随机唤醒一个wait的线程;**notify All:**唤醒所有wait的线程

9.Synchronized底层原理

  • Synchronized依赖于JVM内部的监视器对象来实现线程同步。使用的时候JVM会自动加锁或者解锁,不需要手动操作。Synchronized加锁代码块时,JVM会通过monitorentermonitorexit两个指令来实现同步

    • monitorenter表示线程正在尝试获取锁对象的Monitor
    • monitorexit表示线程执行完了同步代码块,正在释放锁
  • Synchronized是排他锁,当一个线程获取锁之后,其它线程必须等待该线程释放锁之后才能获得锁。

10.Synchronized锁升级的过程

  • 在JDK1.6的时候,为了提高Synchronized的性能,引入了锁升级的机制,从低开销的锁逐步升级到高开销的锁,以最大程度减少锁的竞争。
    • 1.首先初始化对象后默认是无锁状态,表示当前对象未被任何线程锁定;
    • **2.偏向锁:**当第一个线程访问同步代码块时,JVM会将对象头中的标记设置为偏向锁,并记录该线程ID。后续同一线程再次访问同步代码块,JVM会允许直接进入,无需加锁
    • **3.轻量级锁:**当一个线程尝试获取一个已经被其他线程持有的偏向锁时,偏向锁会升级为轻量级锁。当一个线程尝试获取轻量级锁时,它会先自旋一段时间,尝试等待锁被释放。如果在这段时间内锁被释放了,那么这个线程就可以成功获取锁。
    • **4.重量级锁:**当轻量级锁的自旋尝试达到一定阈值,或者检测到多个线程竞争激烈时,JVM会将轻量级锁升级为重量级锁。

11.Synchronized和reentrant lock的区别?

它们两个都是可重入锁:

  1. 底层实现不同: Synchronized是JVM内部的Monitor实现的;而Reentrant Lock是基于AQS实现;
  2. Synchronized可以自动加锁和解锁,而Reentrant Lock只能通过手动加锁和解锁;
  3. Synchronized可以加载代码块和方法上,但是Reentrant Lock只能加载代码块上,但是可以设置公平锁还是非公平锁。
  4. 响应中断不同:Reentrant Lock可以响应中断,解决死锁问题;而Synchronized不能响应中断

12.什么是AQS?

AQS(抽象队列同步器)是Java的一个抽象类,可以用来构建锁和同步类,如ReentrantLock,Semaphore,CountDownLatch,CyclicBarrier。

AQS的原理是:AQS内部有三个核心组件,一个state代表加锁状态初始值为0,一个是获取到锁的线程,一个是阻塞队列。当线程想要获取锁是,会以CAS的形式将State变为1,CAS成功后边将加锁线程设置为自己。当其他线程来竞争锁时会判断state是否为0,如果不是再判断加锁线程是不是自己,如果不是就把自己放入阻塞队列。这个队列是用双向链表实现的(FIFO)。

13.什么是CAS?

  • CAS的全称是比较再交换。在CAS中有三个值:要更新的变量(V),预期值(E),新值(N)。先判断V是否和E相等,如果相等则将V的值设置为N;如果不相等,说明已经有其它线程更新了V,当前线程放弃更新。整个操作过程是原子的,不可中断。

14.什么情况下会产生死锁?如何解决

死锁只有同时满足以下四个条件才会发生:

  • 互斥条件: 多个线程不能同时使用同一个资源

  • 持有并等待条件: 一个线程已经获取资源,并且在等待获取其它线程持有的资源

  • 不可抢占条件: 资源不能被强制从线程中夺走,必须等待线程自己释放

  • 循环等待条件: 发生死锁的时候,两个线程获取资源的顺序形成一个环形链。

避免死锁问题只需要破坏其中一个条件就可以了:让所有线程都按照固定的顺序来申请资源。如果线程无法获取某个资源,可以释放已有的资源再重新尝试申请。

15.线程池执行原理以及核心参数(先讲出核心参数,然后讲执行原理,再讲一下拒绝策略)

线程池的核心参数有:核心线程数,最大线程数,任务队列,线程空闲时间,时间单位,线程工厂和拒绝策略

  • 线程池的执行流程是:
    • 提交线程任务之后判断线程池中是否存在空闲线程,如果存在则分配一个线程给任务,执行线程任务;
    • 如果不存在则会判断当前线程是否超过核心线程数,如果没有超过,则创建一个核心线程来执行任务。
    • 如果超过核心线程数,则会检查工作队列是否满了,如果工作队列未满,则将当前线程任务存入工作队列当中,当线程池中出现空闲线程时,则从工作队列中依次取出线程热任务并执行;
    • 如果工作队列满了,就会判断当前线程是否超过最大线程数,
    • 如果超过则执行拒绝策略;如果**未超过则创建非核心线程来执行任务。
  • 线程池的拒绝策略有四种:
    • **1.AbortPolicy:**默认的拒绝策略:丢弃任务并抛出RejectedExecutionException(拒绝执行异常)
    • **2.DiscardPolicy:**丢弃任务,但不抛出异常
    • 3.DiscardOldestPolicy😗*丢弃工作队列最前面的任务,然后线程池重新执行该任务线程
    • **4.CallerRunsPolicy:**让提交任务的线程自己来执行这个任务

16.核心线程数设置的经验

  • IO密集型:corePoolSize = CPU核数*2 (任务的大部分时间都在等待I/O时(例如:网络请求,数据库查询,读写文件,远程调用API)。在等待期间,线程不会阻塞,不会占用CPU)
  • CPU密集型:corePoolSize = CPU核数+1 (任务的大部分时间都在处理运算、逻辑处理,几乎不会阻塞)

17.Thread Local是什么,是如何实现的?

Thread Local是一种用于实现线程局部变量的工具类。它允许每个线程都拥有自己独立的副本,从而实现线程隔离。

实现:

当我们创建一个Thread Local对象并调用set方法时,其实是在当前线程中初始化了一个ThreadLocalMapThreadLocalMapThreadLocal的一个静态内部类,它内部维护了一个Entry数组,key是Thread Local对象,value是线程的局部变量,这样就相当于为每个线程维护了一个变量副本

18.Thread Local内存泄漏问题以及解决方案

Thread Local Map的key是弱引用,Value是强引用

如果一个线程一直运行,并且value一直指向某个强引用对象,那么这个线程就不会回收,从而导致内存泄漏

解决方案: 当我们使用完Thread Local后,及时调用remove方法释放内存空间。

19.线程池的调优

首先根据任务类型设置核心线程参数,比如IO密集型任务会设置为CPU核心数 * 2的经验值。其次结合线程池动态调整的能力,在流量波动时通过setCorePoolSize平滑扩容,或者直接使用DynamicTp实现线程池参数的自动化调整。最后会通过内置的监控指标建立容量预警机制

20.自己设计一个线程池

首先我会将线程池看作是一个工厂,里面有一群工人(也就是线程),专门用来做任务

当任务来了之后,会判断是否有空闲的工人,如果有则把任务交给它们来执行,如果没有就把任务暂存到一个任务队列里,等工人空闲了再去处理。

如果队列满了,也没有空闲的工人,这个时候就需要考虑扩容,让预备的工人过来干活,但不能超过预定的最大值。如果扩容也解决不了问题,就需要一个拒绝策略,来拒绝这些任务或者报错。