并发-面试必备
2025-09-09 09:58:57 0 举报
AI智能生成
核心内容涵盖多线程编程基础与高级特性,线程安全机制及其实现,同步机制与并发工具,性能优化与调优,以及分布式系统中的并发控制。掌握这些知识点,对于面试官而言,是衡量应聘者是否具备高级开发者能力的标准之一。文件类型可以是PDF格式,格式稳定且跨平台兼容,适合保存此类重要且详实的面试辅导资料。附加修饰语为“深入浅出”,强调内容的实用性,便于读者即使没有深厚背景也能够理解掌握。
作者其他创作
大纲/内容
进程
进程是程序运行时的资源分配的最小单位(以内存为主)
进程可以视为程序的一个实例
上下文切换
cpu切换线程或进程的时候,需要将当前线程的状态信息(如寄存器,程序计数器、局部变量等)提出去存到内存,并且将下一个线程的状态信息信息拿到cpu的告诉缓存中
一般来说,线程切换会比进程快,因为线程不需要加载不同的内存地址空间
上下文切换的代价是很大的,因为每次上下文切换需要5000-20000此时钟周期
等待通知的标准范式
等待通知线程
通过Synchronized加锁同一个对象,然后写一个while循环,如果条件不满足则继续循环,循环里面调用wait方法
通知线程
通过Synchronized加锁同一个对象,然后再业务执行完以后调用notify或notifyAll(通常用这个)(不能再业务执行中间通知,因为这个时候还持有锁)
hash冲突的解决方案
链地址法
HashMap中使用该方法,再1.8以后,链表长度超过8的时候回转化为红黑树
开放寻址法
线性探测
看下一个位置是否为空
ThreadloaclMap中使用该方式
二次探测
伪随机探测
再hash
利用多个hash算法实现,当第一个hash算法的地址不为空,用下一个hash算法计算地址
缓冲区
划分一片缓冲区,如果hash冲突,全部放入缓冲区
CAS
描述
通过CPU提供的的CAS指令实现原子性
问题
ABA
循环开销会很大
只能解决一个共享变量的原子问题
实现
更新基本类型
AtomicInteger
int类型的原子操作
AtomicIntegerArray
以原子操作的方式更新指定位置的int类型数据
LongAdder
1.8出的新的原子类,利用分片的方式占用更多的内存,但是性能比AtomicLong更快
线程会分散到多个片上执行操作,最后完成分片的sum,得到最终结果
引入的原因是:解决高并发环境下 AtomicLong 的自旋瓶 颈问题
LongAccumulator
可以自定义累加函数,而不是简单的加法
更新引用类型
AtomicReference
原子更新引用类
AtomicStampedReference
原子更新应用类型,但是是更具版本来实现,可以解决ABA问题
AtomicMarkableReference
原子更新带有标记位的引用,可以解决ABA问题
并发工具类
AQS
描述
抽象队列同步器,是 Java 中很多同步类的基础构建模块。
AbstractQueuedSynchronizer的简写AQS,它是一个模板类,它提供了统一的标准和基本实现,我们可以通过继承它来实现自己的锁,比如ReentrantLock、ReentrantReadWriteLock就是这样实现的,
设计基于一种 FIFO 等待队列 的思想,主要通过 状态变量 来控制资源的分配和线程的调度。
应用
ReentrantLocal
ReentrantReadWriteLock
Semaphore
锁
ReentrantLock
描述
可重入独占锁,它允许同一个线程多次获取同一个锁
基于Condition可以实现生产者消费者
应用场景
强电影票
多线程的资源写竞争
多线程顺序执行
多线程等待通知
ReentrantReadWriteLock
描述
读写锁
通过内部维护一个状态码实现读写锁的
int类型是4字节,32位二进制,通过将高16位用于读锁的次数记录,低16位记录写锁的次数
允许锁降级,但是不允许锁升级,也就是获取了写锁,可以降级位读锁,但是不能获取了读锁升级位写锁,会导致永久等待
通过holdcounter,记录读锁的重入次数,它是被ThreadLock封装的一个类,是线程安全的
应用
读多写少的场景
信号量:Semaphore
描述
用于控制同时访问某个资源的数量的线程数量
应用
限流
资源池
阻塞队列
ArrayBlockingQueue
基于数组的有界阻塞队列
只有一个锁ReentrantLock,入队和出队都需要加锁
通过下标循环,使队列始终保持先进先出
LinkBlockingQueue
基于链表的无界阻塞队列,默认情况下的大小是Integer.MAX_VALUE,内存不足时会抛出OOM,为避免机器负载过大,最好设置一个最大值
它有两个锁,分别是对应读写锁,保证了数据入队和出队的隔离,性能更好,通常用于线程池
DelayBlockingQueue
支持延迟获取元素的阻塞队列
关键字
Synchronized关键字
描述
Synchronized是一个同步锁,它修饰的对象有
代码块
需要指定加锁对象
方法
加锁对象是这个类的实例对象
静态方法
加锁对象是当前类的class对象
类
加锁对象还是当前类的class对象
对象锁和类锁
对象锁的加锁的目标是一个对象
类锁加锁的目标是一个类
线程并发的时候是实现线程同步
应用
资源竞争的时候,保护共享资源
创建单列的时候,确保多个线程只有一个线程能实例化单列
实现锁的功能
实现生产者-消费者功能
Volatile
描述
Java关键字,用于声明变量再多线程环境中的可见性和禁止指令重排
低开销的同步机制
它比Synchronized更轻,它不会使线程阻塞和唤醒,因此开销更小
应用
可见性
它保证了每次读取变量的时候都从主内存中获取,每次修改变量的时候都同步回主内存,确保了多线程环境中,一个线程修改了变量,其他线程可以立即看到这个变量的修改
状态标志位
volatile经常用于实现状态标志位,例如,控制线程是否应该继续运行的标志。
线程
线程简介
线程是cpu调度的最小单位
线程必须依赖于进程才能存活
线程的创建
线程的创建有很多方式
继承Thread
实现Runnable接口
实现Callable接口
线程池
但是根据Thread的官方介绍,有两种方式创建线程,分别是继承Thread和实现Runnable接口,其他方式都可以看成是这两种方式的派生
线程有几种方式终止
代码执行完以后终止
代码异常终止
调用Thread.stop方法
调用Thread.interrupt设置中断标志位为true
只是通知这个线程,但是是否终端看代码中是否有该标识的处理逻辑
可以通过Thread.currentThread().isInterrupted()获取终端标志位是true或false
可以通过Thread.interrupt去获取终端标志位,并且修改为false
线程的start方法
该方法不能执行两次,会抛异常
因为我们New的线程类,实际上只是一个普通的对象,当我们执行start的时候,实际上是启动一个新的线程并且将该对象和线程做一对一的关联
当调用一次start之后,线程就会进入线程的生命周期
如果再执行一次start,JVM在检查线程的状态的时候会发现异常并抛出异常
线程的状态
初始化
new了一个线程,但是还没有start,这个时候只是jvm堆里面的一个对象,
运行(RUNNABLE):java线程中将运行状态分类两类
ready
就绪状态,等待cpu分配
当Main方法中调用了start方法以后,会启动线程并加入可执行的队列中等待cpu调度
running
执行状态,正在cpu执行
阻塞状态
有且仅有Synchronized关键词可以触发阻塞状态
等待
调用线程的wait()方法将会使线程进入等待的队列中
需要等待其他线程做出通知或中断,才会继续执行
超时等待
和等待的区别是有时间限制,比如Sleep(1),到时间后会自动返回执行
终止
表示线程已经执行完毕
线程调度
协同式线程调度
线程执行时间由自己管理,线程完成任务后通知系统,交出cpu
优点
实现简单:由于不需要开了线程同步等问题,
缺点
会造成线程阻塞
早期因为java也是使用的用户线程,采用的协同式线程调度,所以也叫协程
协程适合IO密集型的业务
如果在计算密集型的业务中使用协程,效率反而会更慢
抢占式线程调度:java线程调度方式
由系统决定每个线程的执行时间
我们已知线程是操作系统层面的实体,java中的线程是和操作系统层面的线程是1:1的
任何语言实现线程都是以下三种方式
内核线程实现(java的实现方式)
java中的线程和操作系统的线程1:1对应,线程的调度完全交给操作系统完成
由于是基于内核线程实现的,所有线程的操作,如:创建、析构、同步都需要系统调用,而系统调用的代价是很大的,需要从内核态和用户态来回切换
java的每个线程都需要一个内核线程支持,所以需要消耗一些内核资源,所以线程的数量是有伤上限的
用户线程实现
内核线程1:n用户线程,也就是代码自己维护用户线程的调度,内核线程没有任何感知,也就不需要内核线程的内核态和用户态切换(如Go语言)
因此操作非常快,且消耗低
缺点是实现非常复杂
混合模式
内核线程n:m用户线程,采用内核线程和用户线程的混合实现,即可以享受了内核线程的功能,又实现了用户线程的自己调度
可以支持大规模的用户线程并发
缺点实现复杂
守护线程
如果正在运行的线程全是守护线程的时候,JVM就会退出,如果又一个不是守护线程的就不会退出
也叫支持线程,支持非守护线程的运行
通过线程的Join方法控制线程的优先级
ThreadLocal
描述
为线程提供一个变量副本,这样每个线程在访问的变量的时候都访问的是自己的变量,这样就保证了线程安全
ThreadLocal和Synchronized的区别是 Synchronized是通过锁的方式实现资源竞争,ThreadLocal是通过给每个线程设置一个变量副本,所以ThreadLocal的效率更高
应用
跨方法进行参数传递
典型的应用:事务的管理中DataSourceTransactionManager设置好数据库连接之后会绑定链接到线程,通过NamedThreadLocal设置绑定数据库链接
线程隔离
避免线程中的数据共享问题
存储用户会话信息
在web应用中,ThreadLocal可以存储用户会话信息,因为每个请求都是一个线程处理,使用ThreadLocal可以保证每个线程访问的是自己的会话信息
减少内存消耗
相对于全局变量,ThreadLocal可以减少内存的消耗,因为线程只分配必要的内存
链路追踪
微服务架构中,因为业务的才分,再一次请求中可能会访问多个服务才能完成请求,链路追踪就是追踪请求访问的路径,通过使用ThreadLocal记录一个访问Id的方式实现
面试必问;并发线程安全问题
什么是线程安全的
总能表现出正确的行为
当多个线程访问某个类时, 不管运行时环境采用何种调度方式或者这些线程 将如何交替执行,
并且在调用代码中不需要任何额外的同步或者协同, 这个类都 能表现出正确的行为,那么就称这个
类是线程安全的。
并且在调用代码中不需要任何额外的同步或者协同, 这个类都 能表现出正确的行为,那么就称这个
类是线程安全的。
反正就是线程不安全的,通常表现为不可预知,有可能正确,有可能错误
如何实现线程安全
线程封闭
把类封装到这个线程里面,只有这个线程可以看见
ThreadLocal是线程封闭的最好方式
栈封闭
简单来说就是局部变量
多个方法访问一个方法的时候,方法内部的局部变量都会拷贝一份到线程栈中
无状态的类
通常是工具类,没有成员变量或者成员变量是不可变的
也就是给变量加上final关键字修饰,但是要注意的是,如果修饰的是一个对象,final关键字保证的是这个对象的引用地址不会变,而不是类里面的属性不会变
加锁或CAS
通常来说就是保证资源的串行,也就是通过加锁的方式保证每次只有一个线程访问资源
当然使用CAS的方式修改资源也是可以的
死锁
死锁的四个条件
互斥条件:某个线程持有资源的时候,其他线程也尝试获取这个资源
请求和保持条件:即线程获取了一个资源,在尝试获取另外一个资源的时候发现被占用了
不剥夺条件:即线程拿到了资源,在完成任务之前不会释放资源
环路等待条件:即多个线程在争夺2个以上的资源
解决死锁的方式:破坏锁条件的任意条件
线程获取资源的顺序一至
使用ReentrantLock尝试拿锁,如果没有拿到全部锁,释放掉手中的锁,并从头还是尝试拿锁,如果设计不合理回出现活锁问题
活锁
描述
因别的线程干扰,线程不停的在获取锁,
通常的解决方案
获取锁失败之后休眠一个随机数,使多个线程的执行出现错峰的情况
锁分类
公平锁和非公平锁
简单来说就是非公平锁允许插队,公平锁不允许,只能顺序拿锁并执行
非公平锁在线程执行的时候会先尝试获取一次锁,如果没有获取成功才会进入等待队列,
可重入锁
即获取了锁之后业务执行递归操作或者另外一个方法需要使用相同的锁,因为自己已经拿到了所以,所以叫重入锁
线程池
核心线程
线程大于核心线程数的时候
当线程数量超过最大核心数的时候,并且任务队列没有任务了,这个时候线程会通过自旋加上CAS来结束多的线程,第一次循环的时候会先获取任务,如果没有拿到任务,会阻塞30秒,30秒后再获取一次任务,没有拿到任务并且大于核心线程数就会通过CAS减少线程数,哪个线程CAS成功,就淘汰哪个线程,剩下的就是核心线程
核心线程数量的设置
CPU密集型任务
建议cpu内核数+1
IO密集型任务
建议通过算法:CPU核心数 *( 1 + 线程等待时间 / 线程运行总时间 )
如果线程等待时间越长,比例会越大,线程的数量就会越大,这是因为,线程等待时间不会利用cpu,如果设置的线程数量过少,可能会导致大部分线程都再等待状态,没有充分利用cpu资源
阻塞队列
通常情况应该使用有界阻塞队列,因为极端情况下无界阻塞队列会导致内存溢出
线程池的状态
线程池的五个状态
RUNNING
正在允许的状态,可以接收新的状态
SHUTDOWN
不能接收新的任务,但是会处理完现有任务
STOP
不能接收新的任务,也不会处理任务队列中的任务
TIDYING
当线程池中的所有线程都停了以后会变为TIDYING状态
TERMINTED
线程完成清理工作之后变为TERMINTED
状态的设计
通过高三位实现状态管理,地位记录线程个数
0 条评论
下一页