[面试题] 多线程、高并发&JUC(上)

本文主要介绍了多线程与高并发相关的面试题目,涵盖了线程与进程的区别、创建线程的几种方式、线程生命周期、线程池的工作原理、线程池的配置以及ThreadLocal的概念和使用。文章强调了线程池的重要性,解释了为何不建议直接使用Executors创建线程池,并提供了如何合理确定线程池线程数的建议。
摘要由CSDN通过智能技术生成

[面试题] 多线程、高并发&JUC(上)

主要包括:多线程、并发编程知识、JUC等相关面试题。

1、进程 vs 线程

  • 进程是程序的一次启动执行,即“正在执行中的程序”。它是系统进行资源分配和调度的一个独立单位。
  • 线程是“程序代码段”的一次顺序执行流程,是进程的一个实体。它是CPU调度的最小单位。
  • 一个进程有一个或多个线程组成,一个进程至少有一个线程。
  • 进程之间是相互独立的,但进程内部的各个线程之间并不完全独立;他们共享进程的方法区内存、堆内存、系统资源(文件句柄、系统信号等)。
  • 线程上下文切换比进程上下文切换要快很多。

2、创建线程的几种方式?

  1. 继承Thread类,然后重写run()方法;
  2. 实现Runnable接口,然后重写run()方法;
  3. 实现Callable接口,重写call方法,创建FutureTask对象组合Callable实例;
  4. 使用线程池,创建线程池对象后让其调用execute()/submit()方法,方法中传入Runnable或Callable实例即可创建线程执行任务。
    // execute方法
    void execute(Runnable command);
    // submit方法
    Future<?> submit(Runnable task);
    <T> Future<T> submit(Callable<T> task);
    

3、说说线程的生命周期

public enum State { // State是Thread内部枚举类
	NEW, // 新建
	RUNNABLE, // 可执行:包括操作系统的就绪和等待两种状态
	BLOCKED, // 阻塞
	WAITING, // 等待
	TIMED_WAITING, // 限时等待
	TERMINATED; // 终止
}
  1. NEW状态
    当线程创建成功,但还未调用start()方法时,Java线程处于NEW状态,对应操作系统线程生命周期的“新建状态”。

  2. RUNNABLE状态

    • 当线程调用了start()方法后,此时Java线程处于RUNNABLE状态,对应操作系统线程生命周期中的“就绪状态”,这时需要等待系统的调度,获得CPU的时间片;
    • 当线程被系统选中,获得了CPU时间片,线程开始占用CPU并执行线程代码,这时对应操作系统线程生命周期的“运行状态”,但在Java线程状态依然是RUNNABLE。
  3. TERMINATED状态
    当处于RUNNABLE状态的线程执行完run()方法或者发生了运行时异常而没有被捕捉,则线程状态将变为TERMINATED,对应操作系统线程生命周期的“终止状态”。

  4. BLOCKED状态
    处于BLOCKED状态下的线程并不会占用CPU资源,以下情况会让线程进入阻塞状态:

    • 线程等待获取锁;
    • 线程发起了阻塞式IO操作,包括磁盘IO、网络IO等,如:等待用户输入内容
  5. WAITING状态
    处于WAITING(无限等待)状态的线程需要被其他线程显式唤醒,才会进入就绪状态,如下方式可以进入WAITING状态:

    • Object.waite()方法,需要Object.notify()/notifyAll()方法唤醒;
    • Thread.join()方法,需要被合入的线程执行完毕才能唤醒;
    • LockSupport.park()方法,需要LockSupport.unpark(Thread)方法唤醒。
  6. TIMED_WAITING状态
    处于TIMED_WAITING(限时等待)状态的线程在指定时间内没有被唤醒,则该线程会被系统自动环境,进入就绪状态。以下方式将进入TIMED_WAITING状态:

    • Thread.sleep(time),限时结束唤醒
    • Object.wait(time),可以调用Object.notify()/notifyAll()唤醒或限时结束唤醒
    • LockSupport.parkNanos(time)/parkUntil(time)方法,可以调用LockSupport.unpark(Thread)方法唤醒或者限时结束唤醒
    • Thread.join(time),限时结束唤醒
      在这里插入图片描述

4、简述线程的常见操作

  • sleep操作:让目前正在执行的线程休眠,让CPU去执行其他任务。
  • interrupt操作
    • 如果线程正处于阻塞状态,则马上退出阻塞,并抛出InterruptedException异常,线程就可以捕捉该异常做处理,然后让线程退出程序;
    • 如果线程正处于运行状态,线程将不受任何影响,继续运行,仅仅是线程的中断标志被设置为true。所以,程序可以在适当的位置通过调用isInterrupted()方法来查看自己是否被中断,并执行退出操作。
  • join操作:在A线程中调用B线程对象的join方法,当代码执行到此处时,需要A线程等待B线程执行结束才能继续往下执行,这就是线程的合并。
  • yield操作:让目前正在执行的线程放弃当前的执行,让出CPU的执行权,使得CPU去执行其他线程。从操作系统线程生命周期来看,线程状态从执行状态变为就绪状态,即放弃本次获得的CPU时间片。

5、谈谈对线程池的认识

  1. 线程池负责线程的创建、维护和分配,对线程对象作统一的调度管理。
  2. 使用线程池有3个好处:
    • 降低资源消耗。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性;通过复用已创建的线程,能降低线程创建和销毁造成的消耗。
    • 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
    • 提高线程的可管理性。线程池提供了一种限制、管理资源的策略,维护一些基本的线程统计信息。还可以对线程资源进行统一的分配、调优和监控。

6、简述线程池的任务调度流程

在这里插入图片描述

  1. 提交任务时,先判断核心线程数(corePoolSize)是否已满
    • 如果还没满,则创建线程执行任务;
    • 如果已经满了,则进入第2步判断队列是否已满
  2. 判断队列是否已满
    • 如果队列未满,则进入队列等待,待工作线程从任务队列中获取任务然后执行;
    • 如果队列已满,则进入第3步判断最大线程数是否已满
  3. 判断最大线程数(maximumPoolSize)是否已满
    • 如果未满,则创建线程(非核心线程)执行任务;
    • 如果已满,则只能执行任务拒绝策略

7、介绍线程池实现类及其重要属性

  1. ThreadPoolExecutor是JUC线程池的核心实现类,继承自抽象类AbstractExecutorService。
  2. ThreadPoolExecutor类相关属性
    // ThreadPoolExecutor构造方法,总共7个参数
    public ThreadPoolExecutor(
    	int corePoolSize, // 核心线程数
    	int maximumPoolSize, // 最大线程数
    	long keepAliveTime, TimeUnit unit, // 空闲线程存活时间间隔(时间 + 单位)
    	BlockingQueue<Runnable> workQueue, // 任务阻塞队列
    	ThreadFactory threadFactory, // 线程工厂
     	RejectedExecutionHandler handler // 拒绝策略
    ){}
    
    // 线程工厂接口
    public interface ThreadFactory {
    	Thread newThread(Runnable r);
    }
    
    // 拒绝策略接口
    public interface RejectedExecutionHandler {
    	void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
    }
    
    • corePoolSize,设置核心线程数
    • maximumPoolSize,设置最大线程数
    • workQueue,任务阻塞队列,存储暂时无法处理的任务,常见的BlockingQueue接口的实现类包括:ArrayBlockingQueue/LinkedBlockingQueue/DelayQueue/SynchronousQueue
    • keepAliveTime+unit,构成空闲线程存活时间,用于设置线程池内线程最大空闲时长;默认情况下,超过了该时间,非核心线程会被回收。如果需要将其应用于核心线程,可以调用allowCoreThreadTimeOut(true); 方法
    • threadFactory,线程工厂,即创建线程实例的类,可以自定义线程工厂类用来更改线程的名称、线程组、优先级等
    • handler
      • 拒绝策略,即拒绝任务提交到线程池执行。
      • 当线程池已经关闭或任务队列已满且maximumPoolSize已满时,任务会被拒绝。
      • RejectedExecutionHandler接口的实现类包括:AbortPolicy(拒绝策略)、DiscardPolicy(抛弃策略)、DiscardOldestPolicy(抛弃最老任务策略)、CallerRunsPolicy(调用者执行策略)

8、为什么不建议使用Executors类创建线程池?

  1. 如果使用Executors类创建线程池,则有4种方式:

    • newSingleThreadExecutor:创建只有一个线程的线程池
    • newFixedThreadPool:创建固定大小的线程池
    • newCachedThreadPool:创建一个不限制线程数量的线程池,任何提交的任务都将立即执行,但空闲线程会得到及时回收
    • newScheduledThreadPool:创建一个可定期或延时执行任务的线程池
  2. 为什么不建议使用以上4种创建线程池的方式?

    • newSingleThreadExecutornewFixedThreadPool默认的任务队列是无界阻塞队列。当任务提交速度远大于执行速度时,会导致任务队列的无限扩大,很可能导致OOM;
    • newCachedThreadPoolnewScheduledThreadPool默认的最大核心线程数为Integer.MAX_VALUE,即可以无限创建线程。当任务提交速度远大于执行速度时,会造成大量的线程启动,很可能造成OOM。
  3. 以下是源码,探讨不推荐的原因,提供参考。

    // Executors类
    public class Executors {
    	// newSingleThreadExecutor
    	// new LinkedBlockingQueue<Runnable>(), 该阻塞队列对象是无界的,
    	// 如果任务提交速度持续大于任务处理速度,就会造成队列中大量的任务等待,
    	// 当队列很大时,很可能导致JVM出现OOM异常,使内存资源耗尽。
    	public static ExecutorService newSingleThreadExecutor() {
            return new FinalizableDelegatedExecutorService
                (new ThreadPoolExecutor(1, 1,
                                        0L, TimeUnit.MILLISECONDS,
                                        new LinkedBlockingQueue<Runnable>()));
        }
    	
    	// newFixedThreadPool,不推荐理由与newSingleThreadExecutor相同,见上
    	public static ExecutorService newFixedThreadPool(int nThreads) {
            return new ThreadPoolExecutor(nThreads, nThreads,
                                          0L, TimeUnit.MILLISECONDS,
                                          new LinkedBlockingQueue<Runnable>());
        }
    
    	// newCachedThreadPool
    	// 最大线程数为Integer.MAX_VALUE,也就是说最大线程数不设上限,可以无限创建线程
    	// 如果任务提交的任务较多,就会造成大量的线程启动,很可能造成OOM异常,甚至导致CPU线程资源耗尽
    	public static ExecutorService newCachedThreadPool() {
            return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                          60L, TimeUnit.SECONDS,
                                          new SynchronousQueue<Runnable>());
        }
    	
    	// newScheduledThreadPool,不推荐理由与newCachedThreadPool相同,见上
    	// 这里提供了ScheduledThreadPoolExecutor类及其父类构造方法的源码,提供参考
    	public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
            return new ScheduledThreadPoolExecutor(corePoolSize);
        }
    }
    
    // ScheduledThreadPoolExecutor类
    public class ScheduledThreadPoolExecutor
            extends ThreadPoolExecutor
            implements ScheduledExecutorService {
    	public ScheduledThreadPoolExecutor(int corePoolSize) {
    			// 调用父类构造方法,第二个参数是设置最大线程数
    	        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
    	              new DelayedWorkQueue());
    	    }
    }
    
    // ThreadPoolExecutor,线程池实现类
    public class ThreadPoolExecutor extends AbstractExecutorService {
    	public ThreadPoolExecutor(int corePoolSize,
    	                              int maximumPoolSize, // 最大线程数
    	                              long keepAliveTime,
    	                              TimeUnit unit,
    	                              BlockingQueue<Runnable> workQueue) {
    	        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
    	             Executors.defaultThreadFactory(), defaultHandler);
    	    }
    }
    

9、如何确定线程池的线程数?

  • IO密集型任务
    • 主要执行IO操作,执行IO操作的时间较长,CPU利用率不高
    • 通常设置最佳线程数为:CPU核数的两倍
  • CPU密集型任务/计算密集型任务
    • 主要执行计算任务,CPU利用率较高
    • 通常设置最佳线程数为:CPU的核数
  • 混合型任务
    • 既执行逻辑运算,有进行大量非CPU耗时操作(如:RPC调用、数据库访问、网络通信等);
    • 通常将其最佳线程数设置为:(线程等待时间与线程CPU时间之比 + 1)* CPU核数

10、谈谈对ThreadLocal类的认识

  1. 如果程序创建了一个ThreadLocal实例,那么在访问该变量时,每个线程都有自己独立的本地值,从而起到线程隔离的作用,规避了线程安全问题。

  2. ThreadLocal实例可以看成是线程专属的变量,不受其他线程干扰,保存着线程的专属数据。

  3. ThreadLocal类的常用方法包括:set(T value) ,T get(),remove(),分别用于设置、获取、移除与当前线程绑定的本地值。

  4. ThreadLocal主要用于线程隔离和跨函数数据传递,如传递请求过程中的用户id、session等。

  5. ThreadLocal底层原理

    • 底层内部结构是一个Map (key, value),但新旧版本的JDK实现有所差异。
    • JDK8之前的版本,key是Thread实例,value是本地值,拥有者是ThreadLocal实例;
    • JDK8开始,key是ThreadLocal实例,value是本地值,拥有者是Thread实例。
      • 当threadLocal.set(T)时,获取当前线程对应的Map,将本地值存储其中;
      • 当threadLocal.get()时,获取当前线程对应的Map,取出存储其中的本地值;
      • 当threadLocal.remove()时,获取当前线程对应的Map,并将threadLocal对应的本地值移除。
      • 这个Map就是ThreadLocal.ThreadLocalMap类,源码如下,提供参考:
      static class ThreadLocalMap {
      	// 存储多个(key, value)
      	private Entry[] table;
      	// 这里的Entry继承了弱引用类WeakReference
      	static class Entry extends WeakReference<ThreadLocal<?>> {
      		Object value;
              Entry(ThreadLocal<?> k, Object v) {
                	// 调用父类构造方法使key成为弱引用对象
                  super(k);
                  value = v;
              }
      	}
      }
      
  6. 使用ThreadLocal的弊端及解决方案

    1. 原理:ThreadLocalMap中Entry的key使用了弱引用。在下次GC发生时,就会将没有被其他强引用之指向、仅被Entry的key所指向的ThreadLocal实例回收。同时将对应Entry中存储的key值设置为null。当ThreadLocal的get()/set(value)/remove()被调用时,ThreadLocalMap内部将清除这些key为null的Entry。
    2. 说明:内存泄露是指不再用到的内存没有及时释放归还系统。对于持续运行的服务进程必须及时释内存,否则内存占用率越来越高会,轻则会影响系统性能,重则导致进程崩溃甚至系统崩溃。
    • 基于上述原理,使用ThreadLocal可能会发生内存泄露,其前提条件是:
      • 线程长时间运行而没有被销毁;
      • ThreadLocal引用已经被设置为null,但后续没有get/set/remove操作,导致key为null的Entry内存没有被释放
    • 在开发过程中,推荐使用static final修饰 + 调用remove()方法,static fina修饰保证变量共享且不可改变,但使得Entry中的key永远不可能为null,可能导致内存泄露,所以可以配合调用remove()方法将threadLocal实例移除。
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值