【并发编程】经典面试题总结-史上最全面试题思维导图总结
2022-10-22 13:51:55 13 举报
AI智能生成
https://github.com/JShengYi/CS_INTERVIEW 「Java学习+面试指南」思维导图,计算机自学指南,包括Java基础、JVM、数据库、mysql、redis、计算机网络、算法、数据结构、操作系统等,后台技术栈/架构师之路/全栈开发社区,阿里,腾讯,百度,美团,头条等春招/秋招/校招/面试
作者其他创作
大纲/内容
继续添加任务到阻塞队列中等待执行
无界队列 LinkedBlockingQueue
首先会被添加到ArrayBlockingQueue 中,ArrayBlockingQueue 满了,会根据maximumPoolSize 的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue 继续满,那么则会使用拒绝策略RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy
有界队列比如 ArrayBlockingQueue
线程池满了
以空间换时间的做法,每个线程可以访问自己内部 ThreadLocalMap 对象内的 value
每个线程中都创建了一个 ThreadLocalMap 对象,
线程局部变量是局限于线程内部的变量
在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉
ThreadLocalMap 中就会出现key为null的Entry。假如我们不做任何措施的话,value 永远无法被GC 回收,这个时候就可能会产生内存泄露。
ThreadLocalMap实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal方法后 最好手动调用remove()方法
ThreadLocal造成内存泄漏
ThreadLocal
BlockingQueue
单线程的线程池
core=max=1,无界队列
newSingleThreadExecutor
堆积的请求处理队列可能会耗费非常大的内存
固定大小的线程池
core=max,都不可回收
newFixedThreadPool
智能的添加新线程
超过大小回收部分空闲(60 秒不执行任务
可缓存的线程池
core是0,所有都可以回收
newCachedThreadPool
周期性执行任务
大小无限的线程池
可能会创建数量非常多的线程,甚至 OOM
定时任务的线程池
newScheduledThreadPool
corePoolSize :核心线程数,线程数定义了最小可以同时运行的线程数量。
maximumPoolSize :线程池中允许存在的工作线程的最大数量
workQueue:当新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,任务就会被存放在队列中。LinkedBlockingDeque<Integer>(数量)
重要参数
keepAliveTime:线程池中的线程数量大于 corePoolSize 的时候,如果这时没有新的任务提交,核心线程外的线程不会立即销毁,而是会等待,直到等待的时间超过了 keepAliveTime才会被回收销毁;释放的是空闲的线程,max-core
unit :keepAliveTime 参数的时间单位。
threadFactory:为线程池提供创建新线程的线程工厂Executors.defaultThreadFactory
ThreadPoolExecutor.AbortPolicy:抛出 RejectedExecutionException来拒绝新任务的处理。
ThreadPoolExecutor.CallerRunsPolicy:调用执行自己的线程运行任务。您不会任务请求。但是这种策略会降低对于新任务提交速度,影响程序的整体性能。另外,这个策略喜欢增加队列容量。如果您的应用程序可以承受此延迟并且你不能任务丢弃任何一个任务请求的话,你可以选择这个策略。
ThreadPoolExecutor.DiscardPolicy:不处理新任务,直接丢弃掉。
ThreadPoolExecutor.DiscardOldestPolicy: 此策略将丢弃最早的未处理的任务请求。
handler :线程池任务队列超过 maxinumPoolSize 之后的拒绝策略
其他参数
首先会用核心线程进行执行,核心满了添加到ArrayBlockingQueue 中,ArrayBlockingQueue 满了,会根据maximumPoolSize 的值增加线程数量,如果增加了线程数量还是处理不过来,ArrayBlockingQueue 继续满,那么则会使用拒绝策略RejectedExecutionHandler 处理满了的任务,默认是 AbortPolicy
ThreadPoolExecutor
创建方式
重用,减少对象创建销毁的开销
降低资源消耗
避免过多资源竞争,避免堵
有效的控制最大并发线程数
提高响应速度
进行统一的分配,调优和监控
提高线程的可管理性
定时执行、定期执行、单线程、并发数控制
附加功能
优点
Executors 工具类的不同方法创建了不同的线程池
Executor 接口对象能执行我们的线程任务
ExecutorService 接口继承了 Executor 接口并进行了扩展,获得任务执行的状态并且可以获取任务的返回值
ThreadPoolExecutor,自定义线程池
Future 表示异步计算的结果,他提供了检查计算是否完成的方法,以等待计算的完成,并可以使用 get()方法获取计算的结果
Executor 和 Executors
execute()只能执行 Runnable 类型的任务。submit()可以执行 Runnable 和 Callable 类型的任务。
submit()方法可以返回持有计算结果的 Future 对象,而execute()没有
submit() 和 execute()
线程池
重量级锁,监视器锁(monitor),依赖于底层的操作系统的 Mutex Lock,需要操作系统帮忙完成,需要从用户态转换到内核态
加到 static 非静态方法和 synchronized(object)代码块
锁住整个对象
sychronized 关键字修饰的方法
更好,不会锁住整个对象,同步的范围越小越好
sychronized 关键字修饰的代码块
加到 static 静态方法和 synchronized(class)代码块
占用的锁是当前类的锁
修饰静态方法
为 uniqueInstance 分配内存空间
初始化 uniqueInstance
将 uniqueInstance 指向分配的内存地址
由于 JVM 具有指令重排的特性,执行顺序有可能变成 1->3->2。指令重排在单线程环境下不会出现问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。例如,线程 T1 执行了 1 和 3,此时 T2 调用 getUniqueInstance() 后发现 uniqueInstance 不为空,因此返回 uniqueInstance,但此时 uniqueInstance 还未被初始化。
把instance声明为volatile之后,对它的写操作就会有一个内存屏障。在它的赋值完成之前,就不用会调用读操作
保证了在一个写操作([1-2-3])完成之前,不会调用读操作(if (instance == null))
uniqueInstance 采用 volatile 关键字修饰也是很有必要的
双重检验锁方式实现单例
monitorenter和monitorexit
防止在同步代码块中线程因异常退出,而锁没有得到释放,这必然会造成死锁(等待的线程永远获取不到锁)
为什么会有两个monitorexit
底层原理
维护一个计数器,当线程获取该锁时,计数器加一,释放锁时,计数器减一,当计数器值为0时,表明该锁未被任何线程所持有,其它线程可以竞争获取锁。
一个线程获取到该锁之后,该线程可以继续获得该锁
synchronized可重入
synchronized
做了多次循环发现还没有获得锁,再阻塞
synchronized 里面的代码执行得非常快,不妨让等待锁的线程不要被阻塞,而是在 synchronized 的边界做忙循环
自旋
对象头里面有一个 threadid 字段,在第一次访问的时候 threadid 为空,jvm 让其持有偏向锁,并将 threadid 设置为其线程 id
再次进入的时候会先判断 threadid 是否与其线程 id 一致,如果一致则可以直接使用此对象,如果不一致,则升级偏向锁为轻量级锁,通过自旋循环一定次数来获取锁
执行一定次数之后,如果还没有正常获取到要使用的对象,此时就会把锁从轻量级升级为重量级锁
锁可以升级但不能降级
无状态锁,偏向锁,轻量级锁和重量级锁
锁升级
synchronized 是悲观锁,属于抢占式,会引起其他线程阻塞。
volatile 共享变量可见性和禁止指令重排序
CAS 是基于冲突检测的乐观锁(非阻塞)
synchronized、volatile、CAS
无条件的、可轮询的(tryLock 方法)、定时的(tryLock 带参方法)、可中断的(lockInterruptibly)、可多条件队列的(newCondition 方法)
只支持非公平锁
操作的应该是对象头中 mark word
不需要手动获取锁和释放锁,使用简单,发生异常会自动释放锁,不会造成死锁
类、方法、代码块
Java内置关键字,JVM层面
支持非公平锁(默认)和公平锁
调用的是 Unsafe 的park 方法加锁,
可以知道有没有成功获取锁
需要自己加锁和释放锁,如果使用不当没有 unLock()去释放锁就会造成死锁
代码块
Java类
Lock
都是可重入锁
synchronized 和 Lock
变量修饰符
保证可见性和禁止指令重排
java.util.concurrent.atomic 包下的类,比如 AtomicInteger
和 CAS 结合,保证了原子性
将当前处理器缓存行的数据写回系统内存;
这个写回内存的操作会使得其他CPU里缓存了该内存地址的数据无效
volatile关键字为域变量的访问提供了一种免锁机制
改变引用指向的数组,将会受到 volatile 的保护,但是如果多个线程同时改变数组的元素,volatile 标示符就不能起到之前的保护作用了
只是一个指向数组的引用,而不是整个数组
数组
volatile
synchronized 、数据库:行锁,表锁,读锁,写锁
悲观锁
java.util.concurrent.atomic 包 CAS
多读的应用类型,提高吞吐量
版本号
乐观锁
乐观锁和悲观锁
CAS 操作中包含三个操作数 —— 需要读写的内存位置(V)、进行比较的预期原值(A)和拟写入的新值(B)。如果内存位置 V 的值与预期原值 A 相匹配,那么处理器会自动将该位置值更新为新值 B。否则处理器不做任何操作。
Compare and Swap
AtomicStampedReference
原有位置A被改成B,又被改回A,误以为没有改变
ABA 问题
资源竞争严重(线程冲突严重)的情况,CAS 自旋的概率会比较大
循环时间长开销大
对多个共享变量操作时,循环 CAS 就无法保证操作的原子性
只能保证一个共享变量的原子操作
问题
CAS
1、互斥条件:所谓互斥就是进程在某一时间内独占资源。
2、请求与保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放。
3、不剥夺条件:进程已获得资源,在末使用完之前,不能强行剥夺。
4、循环等待条件:若干进程之间形成一种头尾相接的循环等待资源关系。
必要条件
尽量使用 Java. util. concurrent 并发类代替自己手写锁。
尽量降低锁的使用粒度,尽量不要几个功能用同一把锁。
尽量减少同步的代码块。
防止死锁
没有被阻塞,由于某些条件没有满足,导致一直重复尝试,失败,尝试,失败。
活锁
无法获得所需要的资源,导致一直无法执行的状态
饥饿
死锁
将暂时获取不到锁的线程加入到队列中
虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)
将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配
使用一个int成员变量来表示同步状态,使用CAS对该同步状态进行原子操作实现对其值的修改
CLH队列锁
Exclusive(独占):只有一个线程能执行,如ReentrantLock。又可分为公平锁和非公平锁:
Share(共享):多个线程可同时执行,如Semaphore/CountDownLatch。Semaphore、CountDownLatch、 CyclicBarrier、ReadWriteLock 我们都会在后面讲到。
AQS定义两种资源共享方式
以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。
可重入锁(ReentrantLock)
任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS(Compare and Swap)减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。
CountDownLatch
读写锁是用来提升并发程序性能的锁分离技术,ReentrantReadWriteLock 是 ReadWriteLock 接口的一个具体实现,实现了读写的分离,读锁是共享的,写锁是独占的,读和读之间不会互斥,读和写、写和读、写和写之间才会互斥,提升了读写的性能。
(1)公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
(2)重进入:读锁和写锁都支持线程重进入。
(3)锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。
ReadWriteLock
AQS AbstractQueuedSynchronizer
Segment 分段锁
segment维护了哈希散列表的若干个桶,每个桶由HashEntry构成的链表
segment继承了ReentrantLock充当锁的角色,为每一个segment提供了线程安全的保障
jdk6
CAS + synchronized
jdk8
ConcurrentHashMap
CAS (compare and swap) + volatile 和 native
自旋锁
CAS 操作——Compare & Set
AtomicMarkableReference(通过引入一个 boolean来反映中间有没有变过)
AtomicStampedReference(通过引入一个 int 来累加来反映中间有没有变过)
ABA 问题
原子操作类
并发关键字
Atomic、synchronized(LOCK)
原子性
可见性
volatile
处理器可能会对指令进行重排序
有序性
三要素
提高 CPU 的利用率
线程越多占用内存也越多
需要协调和管理
解决竞用共享资源
缺点
优缺点
操作系统资源分配的基本单位
独立的代码和数据空间(程序上下文),程序之间的切换会有较大的开销
进程之间的地址空间和资源是相互独立的
一个进程崩溃后,在保护模式下不会对其他进程产生影响
进程
处理器任务调度和执行的基本单位
线程共享代码和数据空间,每个线程都有自己独立的运行栈和程序计数器(PC),线程之间切换的开销小。
线程是进程的一部分
同一进程的线程共享本进程的地址空间和资源
但是一个线程崩溃整个进程都死掉。所以多进程要比多线程健壮。
线程
进程和线程的区别
一个资源只能被一个线程(进程)占用,直到被该线程(进程)释放
互斥条件
一次性申请所有的资源。
因请求被占用资源而发生阻塞时,对已获得的资源保持不放
请求与保持条件
如果申请不到,可以主动释放它占有的资源
末使用完之前不能被其他线程强行剥夺
不剥夺条件
某一顺序申请资源,释放资源则反序释放
必定会形成一个环路
循环等待条件
继承 Thread 类
run 方法无返回值
实现 Runnable 接口
call 方法有返回值,和Future、FutureTask配合
实现 Callable 接口
使用 Executors 工具类创建线程池
start() 方法用于启动线程,线程是处于就绪状态,并没有运行,当分配到时间片后就可以开始运行了
run() 方法用于执行线程的运行时代码,其实就当成一个 main 线程下的普通方法去执行而已
创建线程
新建(new)
可运行(runnable)
运行(running)
wait(),该线程放入等待队列(waitting queue)中
等待阻塞
获取 synchronized 同步锁失败,该线程放入锁池(lock pool)
同步阻塞
sleep()或 join()或发出了 I/O 请求
其他阻塞
阻塞(block)
死亡(dead)
线程的状态
释放所持有的对象的锁
等待(阻塞)状态, Object类的方法
wait()
由 JVM 确定唤醒哪个线程,而且与优先级无关
唤醒一个处于等待状态的线程
notify
让它们竞争,只有获得锁的线程才能进入就绪状态
唤醒所有处于等待状态的线程
notityAll
一个线程完全可以持有很多锁,你一个线程放弃锁的时候,到底要放弃哪个锁
线程调用wait必须拥有该对象的锁,接着它就会释放这个对象锁并进入等待状态直到其他线程调用这个对象上的 notify()方法。当一个线程需要调用对象的 notify()方法时,它会释放这个对象的锁,以便其他在等待的线程就可以得到这个对象锁。
给其他线程运行机会时不考虑线程的优先级
不释放锁
处于睡眠状态,Thread线程类静态方法
sleep
给相同优先级或更高优先级的线程以运行的机会
从执行状态(运行状态)变为可执行态(就绪状态)
yield
在当前正在执行的线程上运行
sleep()和 yield ()方法是静态的
线程调度
syncrhoized加锁的线程的Object类的wait()/notify()/notifyAll()
ReentrantLock类加锁的线程的Condition类的await()/signal()/signalAll()
通过管道进行线程间通信:1)字节流;2)字符流
线程通信协作方式
可冲入、互斥、实现了lock接口的锁他与sychronized方法具有相同的基本行为和语义
使用重入锁实现线程同步
线程同步
基础知识
一旦垃圾回收器准备释放对象占用的内存,将首先调用该对象的finalize()方法,并且下一次垃圾回收动作发生时,才真正回收对象占用的内存空间
finalize
并发理论
并发编程
0 条评论
回复 删除
下一页