当前位置: 首页 > news >正文

多线程与并发编程 面试专题

csdn

多线程与并发编程 面试专题

  • 线程的基础概念
    • 基础概念
    • 线程的创建
    • 线程的状态
    • 线程的终止方式
    • start 与 run 区别
    • 线程的常用方法
    • 锁的分类
    • 深入synchronized
    • 深入ReentrantLock
    • 死锁问题
  • 阻塞队列
  • 线程池

线程的基础概念

基础概念

  • 进程与线程
    • 进程:指运行中的程序。 比如我们使用钉钉,浏览器,需要启动这个程序,操作系统会给这个程序分配一定的资源(占用内存资源)。
    • 线程CPU调度的基本单位,每个线程执行的都是某一个进程的代码的某个片段。
  • 多线程
    • 单个进程中同时运行多个线程。
    • 多线程的不低是为了提高CPU的利用率。可以通过避免一些网络IO或者磁盘IO等需要等待的操作,让CPU去调度其他线程。这样可以大幅度的提升程序的效率,提高用户的体验。
    • 多线程的局限
      • 如果线程数量特别多,CPU在切换线程上下文时,会额外造成很大的消耗。
      • 任务的拆分需要依赖业务场景,有一些异构化的任务,很难对任务拆分,还有很多业务并不是多线程处理更好。
      • 线程安全问题:虽然多线程带来了一定的性能提升,但是再做一些操作时,多线程如果操作临界资源,可能会发生一些数据不一致的安全问题,甚至涉及到锁操作时,会造成死锁问题。
  • 串行、并行、并发
    • 串行:任务按严格顺序执行,前一个任务完成后再开始下一个。
    • 并行:多个任务真正同时执行,依赖多核CPU或多处理器。
    • 并发:任务交替执行(单核)或同时执行(多核),通过调度模拟“同时性”。
  • 同步异步、阻塞非阻塞
    • 同步:任务按顺序执行,前一个任务未完成时,后续任务必须等待。
    • 异步:任务发起后,不等待其完成,继续执行后续操作,通过回调/通知获取结果。
    • 阻塞:线程在等待某个操作(如I/O、锁)完成时,暂停执行,交出CPU控制权。
    • 非阻塞:线程在等待操作完成时继续执行其他任务,通过轮询或回调检查结果。
    • 同步阻塞:单线程调用阻塞式I/O(如传统文件读取)
    • 同步非阻塞:循环调用非阻塞I/O,不断检查数据是否就绪。
    • 异步阻塞:错误设计(如在异步操作中使用阻塞调用)。
    • 异步非阻塞:异步I/O(如Node.js的fs.readFile),发起请求后继续执行其他任务,通过回调或Promise处理结果。

线程的创建

  • 继承Thread类 重写run方法

    public class MiTest {public static void main(String[] args) {MyJob t1 = new MyJob();t1.start();for (int i = 0; i < 100; i++) {System.out.println("main:" + i);}}
    }
    class MyJob extends Thread{@Overridepublic void run() {for (int i = 0; i < 100; i++) {System.out.println("MyJob:" + i);}}
    }
    
  • 实现Runnable接口 重写run方法

    public class MiTest {public static void main(String[] args) {MyRunnable myRunnable = new MyRunnable();Thread t1 = new Thread(myRunnable);t1.start();for (int i = 0; i < 1000; i++) {System.out.println("main:" + i);}}
    }
    class MyRunnable implements Runnable{@Overridepublic void run() {for (int i = 0; i < 1000; i++) {System.out.println("MyRunnable:" + i);}}
    }
    
    • 匿名内部类方式
      Thread t1 = new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 1000; i++) {System.out.println("匿名内部类:" + i);}}
      });
      
    • lambda方式
      Thread t2 = new Thread(() -> {for (int i = 0; i < 100; i++) {System.out.println("lambda:" + i);}
      });
      
  • 实现Callable 重写call方法,配合FutureTask
    Callable一般用于有返回结果的非阻塞的执行方法,同步非阻塞。

    public class MiTest {public static void main(String[] args) throws ExecutionException, InterruptedException {//1. 创建MyCallableMyCallable myCallable = new MyCallable();//2. 创建FutureTask,传入CallableFutureTask futureTask = new FutureTask(myCallable);//3. 创建Thread线程Thread t1 = new Thread(futureTask);//4. 启动线程t1.start();//5. 做一些操作//6. 要结果Object count = futureTask.get();System.out.println("总和为:" + count);}
    }
    class MyCallable implements Callable{@Overridepublic Object call() throws Exception {int count = 0;for (int i = 0; i < 100; i++) {count += i;}return count;}
    }
    
  • 基于线程池构建线程

    // 创建线程池
    ExecutorService threadPool = Executors.newFixedThreadPool(10);
    while(true) {threadPool.execute(new Runnable() { // 提交多个线程任务,并执行@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + " is running ..");try {Thread.sleep(3000);} catch (InterruptedException e) {e.printStackTrace();}}});
    }
    

线程的状态

xc

  • 新建状态(NEW):当程序使用 new 关键字创建了一个线程之后,该线程就处于新建状态,此时仅由 JVM 为其分配内存,并初始化其成员变量的值
  • 就绪状态(RUNNABLE):当线程对象调用了 start()方法之后,该线程处于就绪状态。 Java 虚拟机会为其创建方法调用栈和程序计数器,等待调度运行。
  • 运行状态(RUNNING)就绪状态的线程获得了 CPU开始执行 run()方法的线程执行体,则该线程处于运行状态。
  • 阻塞状态(BLOCKED):线程因为某种原因放弃了 cpu 使用权,也即让出了 cpu timeslice,暂时停止运行。直到线程进入可运(runnable)状态,才有机会再次获得 cpu timeslice 转到运行(running)状态。
    阻塞的情况分三种:
    • 等待阻塞(o.wait->等待对列):运行(running)的线程执行 o.wait()方法, JVM 会把该线程放入等待队列(waitting queue)中。
    • 同步阻塞(lock->锁池):运行(running)的线程在获取对象的同步锁时,若该同步锁被别的线程占用,则 JVM 会把该线程放入锁池(lock pool)中。
    • 其他阻塞(sleep/join):运行(running)的线程执行Thread.sleep(long ms)或 t.join()方法,或者发出了 I/O 请求时,JVM 会把该线程置为阻塞状态。当 sleep()状态超时、 join()等待线程终止或者超时、或者 I/O处理完毕时,线程重新转入可运行(runnable)状态。
  • 等待状态(WATING)调用wait方法就会处于WAITING状态,需要被手动唤醒。
  • 时间等待状态(TIMED_WATING)调用sleep方法或者join方法,会被自动唤醒,无需手动唤醒。
  • 结束状态(TERMINATED):线程会以下面三种方式结束,结束后就是死亡状态。
    • run()或 call()方法执行完成,线程正常结束。异常结束。
    • 线程抛出一个未捕获的 Exception 或 Error。调用 stop。
    • 直接调用该线程的 stop()方法来结束该线程—该方法通常容易导致死锁,不推荐使用。

线程的终止方式

  • 正常运行结束:程序运行结束,线程自动结束。
  • 使用volatile修饰的共享变量(很少会用):通过修改共享变量在破坏死循环,让线程退出循环,结束run方法。
  • Interrupt 方法结束线程
    • 线程处于阻塞状态:如使用了 sleep,同步锁的 wait,socket 中的 receiver,accept 等方法时,会使线程处于阻塞状态。当调用线程的interrupt()方法时,会抛出 InterruptException 异常。阻塞中的那个方法抛出这个异常,通过代码捕获该异常,然后 break 跳出循环状态,从而让我们有机会结束这个线程的执行。 通常很多人认为只要调用 interrupt 方法线程就会结束,实际上是错的, 一定要先捕获InterruptedException 异常之后通过 break 来跳出循环,才能正常结束 run 方法。
    • 线程未处于阻塞状态:使用 isInterrupted()判断线程的中断标志来退出循环。当使用interrupt()方法时,中断标志就会置 true,和使用自定义的标志来控制循环是一样的道理。
  • stop 方法终止线程(线程不安全):强制让线程结束。

start 与 run 区别

  • start() 方法来启动线程,真正实现了多线程运行。这时无需等待 run 方法体代码执行完毕,可以直接继续执行下面的代码。
  • 通过调用 Thread 类的 start()方法来启动一个线程, 这时此线程是处于就绪状态, 并没有运行。
  • 方法 run()称为线程体,它包含了要执行的这个线程的内容,线程就进入了运行状态,开始运行 run 函数当中的代码。 Run 方法运行
    结束, 此线程终止。然后 CPU 再调度其它线程。

线程的常用方法

  • notify()notifyAll()有什么区别

    • 唤醒的线程数量
      • notify():随机唤醒 一个 正在该对象上调用 wait() 的线程(无法指定具体唤醒哪个线程)。
      • notifyAll():唤醒 所有 正在该对象上调用 wait() 的线程。
    • 适用场景
      • notify():当所有等待线程的 逻辑等价(即任何一个线程被唤醒都能完成后续任务)时,使用 notify() 更高效。
      • notifyAll():当等待线程的 条件不同(需要确保所有等待线程都有机会重新检查条件)时,使用 notifyAll() 更安全。
    • 潜在风险
      • notify() :如果多个线程等待的条件不同,可能唤醒错误的线程(例如生产者唤醒了另一个生产者),导致某些线程永远无法被唤醒(线程饥饿死锁)。
      • notifyAll():唤醒所有线程会增加锁竞争,可能降低性能(但在现代 JVM 中影响较小)。
  • sleep()wait() 有什么区别

    • 所属类与调用对象
      • sleep()
        • 属于 Thread 类的静态方法。
        • 直接通过 Thread.sleep(ms) 调用,作用于当前线程。
      • wait()
        • 属于 Object 类的实例方法。
        • 必须在同步块(synchronized)中调用,作用于某个对象(如 obj.wait())。
    • 锁的行为
      • sleep()
        • 不释放锁:线程休眠期间,仍然持有对象的锁(如果已获得)。
        • 其他线程无法进入该对象的同步块。
      • wait()
        • 释放锁:调用后线程会释放对象的锁,允许其他线程获取锁并执行同步代码。
        • 线程进入等待队列,直到被 notify()/notifyAll() 唤醒或超时。
    • 使用场景
      • sleep()
        • 单纯让线程暂停执行一段时间,不涉及线程间协作。
        • 示例:模拟耗时操作,定时任务间隔。
      • wait()
        • 用于线程间通信,需结合 notify()/notifyAll() 实现条件等待。
        • 示例:生产者-消费者模型中,消费者等待队列有数据。
    • 唤醒机制
      • sleep():休眠结束后自动恢复,无需外部唤醒(除非被 interrupt() 中断)。
      • wait():必须通过其他线程调用 notify()/notifyAll() 唤醒,或等待超时(若指定了超时时间)。
  • 异常处理

    • 共同点:两者都可能抛出 InterruptedException(当线程在等待/休眠期间被中断时)。
    • 区别
      • wait() 必须在同步块中调用,否则抛IllegalMonitorStateException。
      • sleep() 无此限制。
  • 线程状态

    • sleep():线程进入 TIMED_WAITING 状态(若指定时间)或 WAITING(若时间无限,但实际 sleep() 必须指定时间)。
    • wait():线程进入 WAITING 状态(无超时)或 TIMED_WAITING 状态(有超时)。
  • Java中interruptedisInterrupted方法的区别

    • 方法定义与作用对象

      方法类型作用对象描述
      Thread.interrupted()静态方法当前执行线程检查并清除当前线程的中断状态。
      thread.isInterrupted()实例方法调用该方法的线程对象仅检查线程的中断状态,不修改状态。
  • Thread类中的yield方法有什么作用

    • Thread.yield() 是一个用于线程调度的静态方法,其主要作用是 提示当前线程让出CPU资源,允许其他线程(尤其是优先级相同或更高的线程)有机会执行。

锁的分类

  • 悲观锁
    • 核心思想:“先加锁,再操作
      默认认为并发操作一定会发生冲突,因此在操作数据前先加锁,确保独占访问。
    • 实现方式
      • 代码层面synchronized关键字、ReentrantLock等。
      • 数据库层面SELECT ... FOR UPDATE(行锁、表锁)、事务隔离级别(如串行化)。
    • 特点
      • 优点:保证强一致性,避免数据冲突。
      • 缺点:加锁带来额外开销,可能引发线程阻塞、死锁,降低并发性能。
    • 适用场景
      • 写操作频繁,冲突概率高(如银行转账)。
      • 需要严格保证数据一致性(如支付系统)。
  • 乐观锁
    • 核心思想:“先操作,再检查
      默认认为冲突概率低,允许并发操作,但在提交时检查数据是否被修改,若冲突则重试或回滚。
    • 实现方式
      • 版本号机制:数据表增加version字段,更新时检查版本是否匹配(UPDATE ... SET version=version+1 WHERE id=1 AND version=old_version)。
      • CAS(Compare and Swap):Java中的AtomicIntegerAtomicReference等原子类。
    • 特点
      • 优点:无锁操作,高并发场景下吞吐量高。
      • 缺点:冲突频繁时重试成本高,可能引发ABA问题(需通过版本号或时间戳解决)。
    • 适用场景
      • 读多写少,冲突概率低(如库存扣减、点赞计数)。
      • 需要高并发性能(如缓存系统、计数器)。
  • 可重入锁
    • 重入性:允许同一个线程多次获取同一把锁。
    • 计数器机制:内部维护一个计数器,记录锁被同一个线程获取的次数。
      • 每次 lock() 时,计数器 +1
      • 每次 unlock() 时,计数器 -1
      • 计数器归零时,锁才被完全释放。
    • 避免自死锁:线程在递归调用或嵌套同步代码块中不会阻塞自己。
    • 应用场景
      • 递归函数中的同步代码。
      • 对象方法之间嵌套调用(如 methodA() 调用 methodB(),二者都需要同步)。
      • 需要灵活控制锁的获取和释放的场景(如 Java 的 ReentrantLock)。
  • 不可重入锁
    • 不可重入性:同一线程重复获取锁时会被阻塞,导致死锁。
    • 简单实现:通常没有记录持有者线程或重入次数的机制。
    • 低开销:实现简单,但使用时需谨慎。
    • 应用场景
      • 简单同步场景(无嵌套或递归需求)。
      • 需要最小化锁开销的场景(需自行确保不会重入)。
  • 公平锁
    • 顺序保证:严格按照线程请求锁的顺序分配锁(先到先得)。
    • 避免饥饿:所有线程最终都能获取锁,不会无限等待。
    • 性能开销:需要维护队列管理请求顺序,上下文切换频繁,吞吐量较低。
    • 实现机制
      • 通过队列(如 CLH 队列)记录等待线程,按顺序唤醒队首线程。
      • Java 中可通过 ReentrantLock(true) 创建公平锁。
  • 非公平锁
    • 允许插队:新请求的线程可以直接尝试抢占锁,无需排队。
    • 高吞吐量:减少线程切换开销,性能更高。
    • 可能饥饿:某些线程可能长期无法获取锁(尤其在竞争激烈时)。
    • 实现机制
      • 线程直接尝试通过 CAS(Compare and Swap)抢占锁,失败后再进入队列等待。
      • Java 中 ReentrantLock 默认是非公平锁。
  • 互斥锁
    • 独占访问:同一时间仅允许一个线程持有锁并访问资源。
    • 写操作优先:适用于需要修改共享资源(如变量、文件、内存)的场景。
    • 强一致性:确保临界区操作的原子性,避免数据竞争。
    • 实现机制
      • 原子操作:通过硬件支持的原子指令(如 CAS)实现锁的获取与释放。
      • 阻塞等待:未获取锁的线程进入阻塞状态,直到锁被释放后被唤醒。
    • 典型应用场景
      • 修改全局变量或共享数据结构。
      • 文件写入、数据库更新等写操作。
      • 需要严格保证操作原子性的场景(如转账)。
  • 共享锁
    • 并发读取:允许多个线程同时持有锁并读取资源。
    • 写互斥:若存在写操作,则所有读/写线程必须等待。
    • 读写分离:通常与互斥锁结合使用,形成 读写锁(Read-Write Lock)。
  • 实现机制
    • 计数器管理:记录当前持有锁的读线程数量。
    • 写锁优先:写锁请求会阻塞后续读锁,确保写操作不被饥饿。
  • 典型应用场景
    • 读多写少的场景(如缓存、配置信息读取)。
    • 数据库查询优化(共享锁允许多个事务并发读同一数据)。
    • 文件读取(多个线程可同时读取文件内容)。

深入synchronized

深入ReentrantLock

死锁问题

  • 死锁产生的必要条件
    • 互斥(Mutual Exclusion):资源一次只能被一个线程占用。
    • 请求与保持(Hold and Wait):线程持有至少一个资源,同时请求其他线程持有的资源。
    • 不可剥夺(No Preemption):资源只能由持有它的线程主动释放,不能被强制剥夺。
    • 循环等待(Circular Wait):多个线程形成环形等待链,每个线程都在等待下一个线程释放资源。
  • 常见死锁场景
    • 场景1:嵌套锁顺序不一致
      // 线程1
      synchronized (lockA) {synchronized (lockB) { ... }
      }// 线程2
      synchronized (lockB) {synchronized (lockA) { ... } // 可能导致死锁
      }
      
    • 场景2:生产者-消费者模型中的资源竞争
      多个线程在共享队列中同时获取插入和删除的锁。
    • 场景3:数据库事务中的行级锁
      多个事务以不同顺序更新相同的数据行。
  • 如何检测死锁
    • JVM工具
      • 使用 jstack <pid> 导出线程栈,查找 Found one Java-level deadlock
      • 使用 jconsoleVisualVM 查看线程状态。
    • 代码检测
      ThreadMXBean bean = ManagementFactory.getThreadMXBean();
      long[] threadIds = bean.findDeadlockedThreads();
      if (threadIds != null) {System.out.println("检测到死锁!");
      }
      
  • 死锁预防与解决
    • 方法1:统一加锁顺序
      • 核心思想:确保所有线程按相同的顺序请求资源。
      • 示例:在转账场景中,按账户哈希值排序锁。
        void transfer(Account from, Account to, int amount) {Account first = from.hashCode() < to.hashCode() ? from : to;Account second = from.hashCode() < to.hashCode() ? to : from;synchronized (first) {synchronized (second) {// 转账逻辑}}
        }
        
    • 方法2:避免嵌套锁
      • 尽量减少锁的作用域,使用无锁设计(如CAS操作)或线程安全容器(如ConcurrentHashMap)。
    • 方法3:设置超时等待
      • 使用 tryLock 替代 synchronized,设定超时时间。
        Lock lockA = new ReentrantLock();
        Lock lockB = new ReentrantLock();
        if (lockA.tryLock(1, TimeUnit.SECONDS)) {try {if (lockB.tryLock(1, TimeUnit.SECONDS)) {try { ... } finally { lockB.unlock(); }}} finally { lockA.unlock(); }
        }
        
    • 方法4:破坏不可剥夺条件
      • 允许系统强制回收资源(需谨慎,可能导致数据不一致)。
    • 方法5:使用线程池隔离
      • 将不同任务分配到独立的线程池,避免资源竞争。

阻塞队列

  • 阻塞队列原理
    • 队列中没有数据的情况下,消费者端的所有线程都会被自动阻塞(挂起),直到有数据放入队列。
    • 队列中填满数据的情况下,生产者端的所有线程都会被自动阻塞(挂起),直到队列中有空的位置,线程被自动唤醒。
  • 阻塞队列的主要方法
    阻塞队列
  • 阻塞队列分类
    • ArrayBlockingQueue :由数组结构组成的有界阻塞队列。
    • LinkedBlockingQueue :由链表结构组成的有界阻塞队列。
    • PriorityBlockingQueue :支持优先级排序的无界阻塞队列。
    • DelayQueue:使用优先级队列实现的无界阻塞队列。
    • SynchronousQueue:不存储元素的阻塞队列。
    • LinkedTransferQueue:由链表结构组成的无界阻塞队列。
    • LinkedBlockingDeque:由链表结构组成的双向阻塞队列

线程池

  • 线程池分类

    • newCachedThreadPool创建一个可根据需要创建新线程的线程池,但是在以前构造的线程可用时将重用它们。
    • newFixedThreadPool创建一个可重用固定线程数的线程池,以共享的无界队列方式来运行这些线。
    • newScheduledThreadPool创建一个线程池,它可安排在给定延迟后运行命令或者定期地执行。
    • newSingleThreadExecutor:Executors.newSingleThreadExecutor()返回一个线程池(这个线程池只有一个线程) ,这个线程池可以在线程死后(或发生异常时)重新启动一个线程来替代原来的线程继续执行下去。
  • 线程池配置属性

    • 核心线程数(corePoolSize)
      • 作用:线程池中始终保持的最小线程数,即使这些线程处于空闲状态。
      • 注意事项
        • 默认情况下,核心线程不会因空闲而被回收(除非配置了allowCoreThreadTimeOut)。
        • 适用于处理常规任务负载。
    • 最大线程数(maximumPoolSize)
      • 作用:线程池允许创建的最大线程数量。当工作队列已满且当前线程数小于最大线程数时,会创建新线程处理任务。
      • 注意事项
        • 需根据系统资源(如CPU核数、内存)和任务类型(CPU密集型或IO密集型)合理设置。
        • 若设置为与corePoolSize相同,则线程池退化为固定大小线程池。
    • 空闲线程存活时间(keepAliveTime)
      • 作用:非核心线程(超出corePoolSize的线程)在空闲状态下的存活时间。超过该时间后,线程会被终止回收。
      • 单位:通常与TimeUnit配合使用(如秒、毫秒)。
      • 注意事项
        • 若允许核心线程超时(通过allowCoreThreadTimeOut(true)),核心线程也会受此参数影响。
    • 工作队列(workQueue)
      • 作用:用于保存等待执行任务的阻塞队列。
        常见类型:
        • 无界队列(如LinkedBlockingQueue):可能导致内存溢出。
        • 有界队列(如ArrayBlockingQueue):需合理设置容量。
        • 同步移交队列(如SynchronousQueue):不存储任务,直接将任务交给线程。
      • 选择策略
      • 任务量波动大时,优先使用有界队列避免资源耗尽。
      • 高吞吐场景可使用LinkedBlockingQueue,快速响应场景可使用SynchronousQueue
    • 线程工厂(threadFactory)
      • 作用:自定义线程创建方式,可设置线程名称、优先级、是否为守护线程等。
      • 示例
        ThreadFactory factory = r -> {Thread t = new Thread(r);t.setName("my-pool-" + t.getId());return t;
        };
        
    • 拒绝策略(RejectedExecutionHandler)
      • 作用:当线程池无法接受新任务(队列已满且线程数达上限)时的处理策略。
        常见策略:
        • AbortPolicy(默认):抛出RejectedExecutionException
        • CallerRunsPolicy:由提交任务的线程直接执行任务。
        • DiscardPolicy:静默丢弃被拒绝的任务。
        • DiscardOldestPolicy:丢弃队列中最旧的任务,并重新提交被拒绝的任务。
      • 自定义策略:可扩展RejectedExecutionHandler接口实现特定逻辑(如记录日志或重试)。
    • 其他配置
      • allowCoreThreadTimeOut:允许核心线程因空闲超时被回收(需keepAliveTime > 0)。
      • 预热核心线程:通过prestartAllCoreThreads()提前启动所有核心线程。

http://www.mrgr.cn/news/94477.html

相关文章:

  • HarmonyOS NEXT - 电商App实例三( 网络请求axios)
  • MFC控件按钮的使用
  • Docker 》》Docker Compose 》》network 网络 compose
  • 保姆级离线TiDB V8+解释
  • STAR Decomposition 一种针对极端事件的信号分解方法 论文精读加复现
  • ctf工具——Audacity的安装和使用
  • OpenEuler-22.03-LTS上利用Ansible轻松部署MySQL 5.7
  • 【论文笔记】Contrastive Learning for Compact Single Image Dehazing(AECR-Net)
  • 机器学习(吴恩达)
  • 如何在Futter开发中做性能优化?
  • 一篇博客搞定时间复杂度
  • Spring 中 BeanPostProcessor 的作用和示例
  • 【第七节】windows sdk编程:Windows 中的对话框
  • 数据结构——最短路(BFS,Dijkstra,Floyd)
  • 机器学习与深度学习中模型训练时常用的四种正则化技术L1,L2,L21,ElasticNet
  • 【第六节】windows sdk编程:Windows 中的资源
  • 静态时序分析:SDC约束命令set_sense详解
  • C++初阶——类和对象(一)
  • 万字长文详解嵌入式电机软件开发
  • 搭建基于flask的web应用框架