线程与多线程/异步与同步¶
进程和线程的区别?¶
1. 资源分配 vs 执行单元 - 进程是资源分配的基本单位,每个进程都有独立的地址空间、文件描述符、内存等资源 - 线程是CPU调度的基本单位,是程序执行的最小单位,一个线程只能属于一个进程,但一个进程可以拥有多个线程 2. 地址空间 - 进程拥有独立的虚拟地址空间 - 线程共享所属进程的地址空间,这使得线程间通信更高效 3. 相互影响 - 进程间相互独立,一个进程崩溃不会影响其他进程 - 线程间共享资源,一个线程崩溃会导致整个进程崩溃,进而影响同进程的其他线程 4. 开销 - 进程创建和切换的开销较大 - 线程创建和切换开销较小,因为共享地址空间
协程是一种用户态的轻量级线程,协程的调度完全由用户控制 协程拥有自己的寄存器上下文和栈。协程调度切换时,将寄存器上下文和栈保存到其他地方,在切换回来的时候,恢复先前保存的寄存器上下文和栈,没有内核切换开销所以非常快
- 协程和线程之间的区别
- 一个线程可以有多个协程,一个进程也可以单独拥有多个协程
- 线程进程都是同步机制,而协程则是异步
- 协程能保留上一次调用时的状态,每次过程重入时,就相当于进入上一次调用 的状态
进程是分配资源的基本单位,那么这个资源指的是什么?¶
进程作为资源分配的基本单位,这里提到的"资源"主要包括以下几个方面: 1. 内存资源 - 进程的虚拟地址空间,包括代码段、数据段、堆、栈等 2. 文件资源 - 进程打开的文件描述符、文件表项等 3. I/O设备资源 - 进程正在使用的I/O设备(如打印机、显示器等) 4. 进程控制块(PCB) - 操作系统为每个进程维护的进程控制块,包含进程ID、状态、优先级、寄存器值等管理信息 5. 信号量、共享内存等进程间通信资源 简单来说,进程从操作系统获得的资源包括内存空间、文件句柄、I/O设备访问权限以及进程管理所需的各种内核数据结构。
死锁是什么?如何避免死锁?¶
死锁的定义: 死锁是指两个或多个进程在执行过程中,因争夺资源而造成一种相互等待的现象,若无外力干预,它们都将无法推进下去。例如:线程A持有资源X并等待资源Y,而线程B持有资源Y并等待资源X,两者都在等待对方释放资源,形成循环等待。
产生死锁的四个必要条件(同时满足才会发生死锁): 1. 互斥条件:资源一次只能被一个线程使用 2. 持有并等待条件:线程在持有已有资源的同时,等待其他资源 3. 不可剥夺条件:已分配的资源不能被强制抢走,只能主动释放 4. 循环等待条件:存在一种进程资源的循环等待链
避免死锁的方法: 1. 破坏互斥条件——不太可行,因为有些资源本身具有排他性(如打印机),无法共享 2. 破坏持有并等待条件——可以在进程启动前一次性申请所有所需资源,但这会导致资源浪费严重 3. 破坏不可剥夺条件——当进程申请新资源失败时,释放已持有的资源,但实现复杂 4. 破坏循环等待条件——按顺序申请资源,为资源编号,规定进程必须按编号递增的顺序申请资源 5. 银行家算法——在资源分配前,预先给进程分配资源,然后判断当前是否处于安全状态(查 看资源池所剩资源是否能满足所有进程中所需资源最小的进程的需求),如果是,那么 对这个进程的资源分配有效,否则无效 6. 死锁检测与恢复——允许死锁发生,但通过定期检测死锁并采取措施恢复(如回滚、终止进程等) 实际开发中,最佳实践是:按相同的顺序获取锁、设置锁的超时时间、尽量减少锁的粒度。
线程池最大10,如果来了第11个会怎样?¶
- 按拒绝策略执行对应行为
- CallerRunsPolicy,使用线程池的调用者所在的线程去执行被拒绝的任务,除非线程池被停止或者线程池的任务队列已有空缺。
- AbortPolicy,直接抛出一个任务被线程池拒绝的异常。
- DiscardPolicy,不做任何处理,静默拒绝提交的任务。
- DiscardOldestPolicy,抛弃最老的任务,然后执行该任务。
- 自定义拒绝策略,通过实现接口可以自定义任务拒绝策略。
java线程池参数有哪些(7个)¶
- 核心线程数
- 最大线程数
- 空闲线程存活时间(超时销毁)
- 时间的单位
- 工作队列(存放任务)
- 线程工厂(给线程取名)
- 拒绝策略(调用者执行、抛异常、静默拒绝、抛弃老任务、自定义拒绝)
java 实现多线程有几种方式¶
面试官您好,Java实现多线程主要有四种方式,我来逐一说明:
第一种,继承Thread类 创建一个类继承Thread,重写run方法,在main方法中调用实例的start方法启动线程。不过这种方式我不太推荐,因为Java是单继承的,继承了Thread就没法继承其他类了。
第二种,实现Runnable接口 这是更常用的方式,创建类实现Runnable接口,重写run方法,然后把实例传给Thread构造方法,调用start启动。这样做的好处是可以避免单继承的限制,还能实现多个接口。
第三种,实现Callable接口 这个和Runnable类似,但它的call方法有返回值,还能抛出异常。使用时要把Callable实例包装成FutureTask,再传给Thread构造方法,通过FutureTask的get方法获取执行结果。
第四种,使用线程池 这是生产环境最推荐的方式,通过ThreadPoolExecutor构造方法创建。线程池的好处很多:一是降低资源消耗,线程可以复用;二是提高响应速度,任务来了不用等线程创建;三是方便管理,能统一分配、调优和监控线程。
补充一下:为什么不能直接调用run方法? 因为调用start方法才会真正启动一个新线程,让线程进入就绪状态等待CPU调度。如果直接调用run方法,那就只是普通的方法调用,不会以多线程方式执行,还是在当前线程中同步执行的。
再说说多线程的优缺点: 优点很明显,当一个线程阻塞或等待时,CPU可以去执行其他线程,能大大提高CPU利用率。 缺点主要有两个:一是频繁的上下文切换会影响执行速度;二是多线程编程容易出现死锁问题。
线程池有什么用,如何创建线程池¶
好处:
○ 降低资源消耗,通过重复利用已创建的线程降低线程创建和销毁造成的消耗
○ 提高响应速度,当任务到达时,任务可以不需要等待线程就能立即执行
○ 提高线程的可管理性,线程是稀缺资源,如果无限制的创建,不仅消耗资源而 且降低系统的稳定性,使用线程池可以进行线程的统一分配,调优和监控
• Executor框架的主要成员:ThreadPoolExecutor、ScheduledThreadPoolExecutor、future 接口、Runnable 接口、Callable 接口 和 Executors
创建线程池的两种方式:
第一种,通过ThreadPoolExecutor 构造函数实现(推荐)
通过Executor 框架的工具类Executors来实现我们可以创建三种类型的 ThreadPoolExecutor:(不推荐,容量为Integer.MAX_VALUE,所以容易OOM)
1. CachedThreadPool:返回一个可根据实际情况调整线程数量的线程池,如 果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程
2. FixedThreadPool:返回一个固定线程数量的线程池,可控制线程最大并发 数,超过的线程会在队列中等待
3. SingleThreadExecutor:返回一个只有一个线程的线程池,它只会用唯一的 线程来执行任务,保证所有任务按照执行顺序执行。
ThreadPoolExecutor 构造方法的7个参数: 1. corePoolSize:线程池的核心线程数(最小可以同时运行的线程数) 2. maximumPoolSize:线程池的最大线程数(当任务队列存放的任务达到队列容 量的时候,当前可以同时运行的线程数量变为最大线程数) 3. keepAliveTime:当线程数大于核心线程数时,多余的空闲线程存活的最长时间 (如果此时没有新的任务提交,核心线程外的线程不会立即销毁,而是等到这么长 时间,会被回收销毁) 4. unit:时间单位 5. workQueue:任务队列,用来存储等待执行任务的队列(当新的任务来的时候, 会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,就会被放到队 列中) 6. threadFactory:线程工厂,用来创建线程 7. RejectedExecutionHandler:拒绝策略,当提交的任务过多而不能及时处理时, 我们可以定制拒绝策略来处理任务
第二种,ScheduledThreadPoolExecutor:用来在给定的延迟后运行任务,或者定期执行任务 (一般不会使用)
synchronized 的原理¶
synchronized 的锁对象会关联一个monitor(监视器,它才是真正的锁对象),这个 monitor 不是我们主动创建的,是JVM的线程执行到这个同步代码块,发现锁对象没有 monitor 就会创建monitor,monitor 内部有两个重要的成员变量owner:拥有这把锁的线 程,recursions:记录线程拥有锁的次数,当一个线程拥有monitor后,其他线程只能等 待。当执行到monitorexit 时,recursions 会减1,当计数器为0时,这个线程会释放锁 • 同步方法在反编译之后,会增加ACC_SYNCHRONIZED修饰。它会隐式调用 monitorenter 和 monitorexit。在执行同步方法前调用monitorenter,在执行完同步方法后 调用monitorexit
monitor 是重量级锁 ○ ObjectMonitor 的函数调用中会涉及内核函数,执行同步代码块,没有竞争到锁。对象会park()被挂起,竞争到锁对象的线程会unpark()唤醒。这个时候就会【存在 操作系统用户态和内核态的转换】,这种切换会消耗大量的系统资源,所以说 synchronized 是 java 语言中一个重量级操作
OSI 七层模型¶
除了synchronized,还有什么方法可以实现线程同步?¶
除了synchronized,还有以下几种方法可以实现线程同步:
第一,使用Lock接口及其实现类 - ReentrantLock(可重入锁):功能比synchronized更强大,支持公平锁/非公平锁、可中断锁等待、多条件变量等 - ReentrantReadWriteLock(读写锁):读操作可以多线程并发执行,写操作独占,适合读多写少场景 - StampedLock:JDK8引入的乐观读锁,性能比ReadWriteLock更好
第二,使用volatile关键字 - 保证变量的可见性,一个线程修改后其他线程立即可见 - 禁止指令重排序,但不保证原子性 - 适合状态标志等简单变量的同步
第三,使用原子变量类 - 如AtomicInteger、AtomicLong、AtomicReference等 - 底层使用CAS(Compare-And-Swap)实现,保证原子性 - 适合计数器、序列生成器等场景
第四,使用ThreadLocal - 为每个线程提供独立的变量副本 - 避免线程间共享数据,从根本上解决线程安全问题 - 适合存储线程上下文信息、数据库连接等
第五,使用并发集合 - ConcurrentHashMap、ConcurrentLinkedQueue、CopyOnWriteArrayList等 - 这些集合内部已经实现了线程安全机制 - 相比Collections.synchronizedXXX性能更好
第六,使用信号量(Semaphore) - 控制同时访问特定资源的线程数量 - 通过acquire()和release()控制并发度
线程池的源码有没有看过?¶
线程池的源码我有看过,主要看了ThreadPoolExecutor的核心实现。
首先,线程池的核心工作流程: - 当提交任务时,会先判断当前工作线程数是否小于核心线程数 - 如果小于,则创建新的核心线程执行任务 - 如果已满,则将任务加入阻塞队列等待 - 如果队列也满了,会判断当前线程数是否小于最大线程数 - 如果小于,则创建临时线程(非核心线程)执行任务 - 如果已达到最大线程数,则执行拒绝策略
其次,我了解到几个关键点: - 线程池使用一个HashSet管理线程(workers),每个worker就是一个线程 - 任务执行使用while循环,不断从阻塞队列中获取任务 - 核心线程和临时线程的回收时机不同:核心线程默认不会被回收(除非allowCoreThreadTimeOut为true),临时线程在空闲超过keepAliveTime后会被回收 - 线程池的ctl变量包含线程池状态和workerCount,高3位存储状态,低29位存储线程数量
补充一个细节: 线程池的execute方法设计得很精妙,用位运算在一个变量里存储了状态和数量,通过ctlOf方法组合,非常高效。
解释一下自定义线程池的核心参数¶
自定义线程池主要有7个核心参数,我来逐一解释:
第一,corePoolSize(核心线程数) - 线程池中保持存活的最少线程数 - 即使这些线程处于空闲状态,也不会被回收(除非设置allowCoreThreadTimeOut) - 用来处理正常负载的任务
第二,maximumPoolSize(最大线程数) - 线程池允许创建的最大线程数量 - 当核心线程都在忙,且任务队列满了之后,会创建新的线程 - 用来应对突发的高负载任务
第三,keepAliveTime(线程存活时间) - 非核心线程空闲后的最大存活时间 - 超过这个时间还没有任务执行,非核心线程会被回收 - 设置为0表示多余线程立即终止
第四,unit(时间单位) - 指定keepAliveTime的单位 - 常用有MILLISECONDS(毫秒)、SECONDS(秒)、MINUTES(分钟)
第五,workQueue(任务队列) - 存放等待执行任务的阻塞队列 - 常用队列:LinkedBlockingQueue(无界队列)、ArrayBlockingQueue(有界队列)、SynchronousQueue(同步队列) - 选择合适的队列对性能影响很大
第六,threadFactory(线程工厂) - 用来创建新线程的工厂 - 可以自定义线程名称、优先级、是否为守护线程等 - 默认使用DefaultThreadFactory
第七,handler(拒绝策略) - 当线程池和队列都满了之后的处理策略 - 常用策略:AbortPolicy(抛异常)、CallerRunsPolicy(调用者执行)、DiscardPolicy(静默拒绝)、DiscardOldestPolicy(抛弃最老任务) - 也可以自定义策略
线程的安全性?以及为了保证安全性,有什么机制?¶
线程安全性是指多个线程访问共享数据时,程序行为始终正确。
线程不安全的表现: - 可见性:一个线程对共享变量的修改,其他线程不可见 - 原子性:复合操作(如i++)不是原子操作,可能被分割 - 有序性:指令重排序可能导致预期外的执行顺序
保证线程安全的机制:
第一,锁机制 - synchronized:内置锁,自动获取/释放,JVM实现 - Lock接口:显式锁,需要手动lock/unlock,功能更丰富
第二,volatile机制 - 保证可见性:写后立即刷新到主内存 - 保证有序性:禁止指令重排序 - 不保证原子性
第三,CAS机制(Compare-And-Swap) - 乐观锁思想,不加锁,通过不断重试 - 原子类(如AtomicInteger)底层使用CAS - 适合竞争不激烈的场景
第四,ThreadLocal机制 - 线程本地存储,每个线程有自己的独立副本 - 从根本上避免共享,自然线程安全
第五,并发容器 - ConcurrentHashMap、CopyOnWriteArrayList等 - 内部已实现同步机制
第六,final关键字 - final修饰的变量在构造函数初始化后,对所有线程可见 - 不可变对象天然线程安全
有没有不用锁的方案呢?¶
有的,不用锁的方案主要有以下几种:
第一,CAS(Compare-And-Swap) - 核心思想:比较当前值与期望值,如果相同则更新 - 三个操作数:内存位置V、期望值A、新值B - 如果V的值等于A,则更新为B,否则什么都不做 - 整个操作是原子的,硬件层面支持
第二,CopyOnWrite(写时复制) - 核心思想:写入时复制,修改时复制一份副本,在副本上修改,修改完成后指针指向新副本 - 读操作不加锁,允许多线程并发读 - 写操作需要复制和替换,适合读多写少场景 - 代表类:CopyOnWriteArrayList、CopyOnWriteArraySet
第三,ThreadLocal(线程本地存储) - 每个线程拥有独立的变量副本 - 线程间不共享数据,自然无需加锁 - 适合存储线程上下文、数据库连接等
第四,无锁数据结构 - 如ConcurrentLinkedQueue等 - 内部使用CAS和周到的算法设计实现线程安全
第五,魔法变量和final - 不可变对象(所有字段都是final且没有逸出) - 天然线程安全,不需要同步
补充说明CAS的ABA问题: - 如果一个值从A变成B再变回A,CAS会认为没有变化 - 解决方案:使用版本号(如AtomicStampedReference)
CAS 和 synchronize有什么区别?¶
CAS和synchronized都是实现线程同步的手段,但它们有很大区别:
*首先,从实现原理上区分:* - CAS是CPU层面的原子指令,通过硬件保证操作的原子性(cmpxchg指令) - synchronized是JVM层面的实现,通过monitorenter/monitorexit字节码实现
其次,从使用方式上区分: - CAS是乐观锁思想,不阻塞线程,失败重试,适合竞争不激烈的场景 - synchronized是悲观锁思想,阻塞线程,一次只能一个线程访问
第三,从特性上区分: - CAS只保证原子性,不保证可见性,需要配合volatile使用 - synchronized既保证原子性,也保证可见性(解锁时刷新主内存)
第四,性能特点: - 低竞争:CAS性能更好,因为无需上下文切换 - 高竞争:synchronized性能更稳定,CAS可能导致大量线程空转 - JDK6之后,synchronized做了大量优化(偏向锁、轻量级锁、自旋锁),性能已大幅提升
第五,死锁风险: - CAS不会死锁,只会重试 - synchronized如果使用不当(如嵌套锁、活锁)可能导致死锁
实际选择建议: - 竞争不激烈、对响应要求高:优先考虑CAS(如AtomicInteger) - 竞争激烈、需保证原子性:考虑synchronized或ReentrantLock - 需要高级功能(公平锁、读写分离):选择JUC包提供的Lock实现
Java线程几种创建方式,获取返回值是什么¶
- 继承Thread类,没有返回值
- 实现Runnable接口,没有返回值
- 实现Callable
接口,要执行Callable任务,需将它包装进一个FutureTask,用get()方法获取返回值,类型是T - 使用线程池(Executor框架),可以有返回值,也可以没有返回值。
Java线程池怎么实现的?用到了哪些参数?¶
用线程池的实现类ThreadPoolExecutor,其构造方法参数:
- corePoolSize(必需):核心线程数。默认情况下,核心线程会一直存活,但是当将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
- maximumPoolSize(必需):线程池所能容纳的最大线程数。当活跃线程数达到该数值后,后续的新任务将会阻塞。
- keepAliveTime(必需):线程闲置超时时长。如果超过该时长,非核心线程就会被回收。如果将 allowCoreThreadTimeout 设置为 true 时,核心线程也会超时回收。
- unit(必需):指定 keepAliveTime 参数的时间单位。常用的有:TimeUnit.MILLISECONDS(毫秒)、TimeUnit.SECONDS(秒)、TimeUnit.MINUTES(分)。
- workQueue(必需):任务队列。通过线程池的 execute() 方法提交的 Runnable 对象将存储在该参数中。其采用阻塞队列实现。
- threadFactory(可选):线程工厂。用于指定为线程池创建新线程的方式。
- handler(可选):拒绝策略。当达到最大线程数时需要执行的饱和策略。
线程安全 synchronized和ReentrantLock有什么区别¶
关于synchronized和ReentrantLock的区别,我总结了以下几点:
首先,从使用方式上区分: - synchronized是Java关键字,在JVM层面实现,使用时自动获取和释放锁 - ReentrantLock是JDK5提供的类,实现了Lock接口,使用时需要手动调用lock()和unlock()方法
其次,从功能特性上区分: - ReentrantLock支持公平锁和非公平锁,而synchronized只支持非公平锁 - ReentrantLock支持可中断锁等待,使用lockInterruptibly()方法,而synchronized不支持 - ReentrantLock可以绑定多个条件变量,使用newCondition()创建,而synchronized只有一个隐式条件 - ReentrantLock可以反复加锁,同一个线程可以多次获取锁(当然也要释放相同次数),synchronized也支持可重入
第三,从性能方面: - JDK5中synchronized性能较差,但JDK6之后做了大量优化(偏向锁、轻量级锁、自旋锁),两者性能差异不大 - 高并发场景下,ReentrantLock可能表现更稳定
第四,从便捷性方面: - synchronized不需要担心忘记释放锁,JVM会自动处理 - ReentrantLock需要finally中释放锁,否则容易死锁
第五,底层实现: - synchronized的字节码指令是monitorenter和monitorexit - ReentrantLock基于AbstractQueuedSynchronizer(AQS)实现,使用CAS和park/unpark实现
实际选择建议: - 普通同步需求优先使用synchronized,因为更简洁安全 - 需要高级特性(公平锁、可中断、多条件)时使用ReentrantLock
乐观悲观锁¶
关于乐观锁和悲观锁,我来说明一下:
首先,悲观锁的核心思想: - 悲观的认为并发访问一定会产生冲突 - 所以每次访问共享资源前都要先加锁 - 典型代表:synchronized、ReentrantLock - 优点:保证数据一致性 - 缺点:阻塞等待,性能开销大,可能死锁
其次,乐观锁的核心思想: - 乐观的认为并发访问很少发生冲突 - 所以不加锁,只在更新时检查数据是否被修改 - 典型实现:CAS(Compare-And-Swap)操作 - 优点:非阻塞,性能好,无死锁风险 - 缺点:只保证原子性,可能ABA问题,需要重试
第三,典型使用场景: - 悲观锁适合写多场景,如:修改用户余额、库存扣减等 - 乐观锁适合读多写少场景,如:统计数据、浏览量等
第四,乐观锁的ABA问题: - 线程1读取值A,线程2将值改为B又改回A,线程1检查发现还是A,以为没变化就更新了 - 解决方案:使用版本号(AtomicStampedReference)或时间戳 - 实际上业务上通常能接受短暂的数据不一致
第五,实际应用举例: - 数据库的乐观锁:version字段,每次更新时version+1,更新时检查version - Java的乐观锁:AtomicInteger的incrementAndGet(),底层用CAS - 分布式场景:使用Redis的watch命令实现乐观锁
Java原生异步操作怎么实现¶
- 最简单使用异步编程的方式就是创建一个 线程 来实现
- Future异步,获取结果时需要阻塞线程,或者不断轮询
- CompletableFuture类 可以通过回调的方式来处理计算结果,实现了异步非阻塞