java多线程面试大纲
2021-12-29 17:41:12 2 举报
AI智能生成
点我主页,涵盖 Java 后面面试所有知识点,本人持续整理,目前已帮助20+人拿到 大厂Offer
作者其他创作
大纲/内容
Java 中的多线程之锁优化
Synchronized
Synchronized 同步锁实现原理
锁优化
锁升级
动态编译实现锁消除 / 锁粗化
减小锁粒度
Lock同步锁
Lock 锁的实现原理
AQS
ReentrantLock 独占锁
ReentrantReadWriteLock 读写锁
StampedLock - 盖章锁
没有大规模使用:因为不可重入
使用乐观锁优化并行操作
什么是乐观锁?
乐观锁的实现原理
CAS
CPU如何实现原子操作
CPU提供了<b>总线锁定</b>和<b>缓存锁定</b>两个机制来保证复杂内存操作的原子性
CAS 频繁会导致 总线风暴问题
优化 CAS 乐观锁
AtomicInteger AtomicLong 内部使用 Unsafe 的 CAS,不断CAS 会造成 CPU 繁忙
LongAdder:
多线程调优
CPU 上下文切换优化
15 | 多线程调优(上):哪些操作导致了CPU上下文切换?
Linux 使用 vmstat 命令发现 CPU 上下文切换程序
16 | 多线程调优(下):如何优化多线程上下文切换? - CPU 高
上下文切换排查工具:
Linux 下 vmstat 命令
java 中的锁
java中有几种锁?
Synchroized
三种应用方式:
修饰实例方法
当前对象
修饰静态方法
类对象
代码块
给定对象
基于 monitorenter 和 monitorexit 指令实现
简单地说,synchronized修饰,表现为两种锁:<br>一种是对调用该方法的对象加锁,俗称<b><font color="#f15a23">对象锁</font></b>或实例锁,<br><ul><li>另一种是对该类对象加锁,俗称<b><font color="#f15a23">类锁</font></b>。</li></ul>
对象锁
类锁
sychronized 的实现原理:
<b><font color="#f15a23">synchronized实现原理<br></font><font color="#c41230">synchronized 在 JVM 的实现原理是基于进入和退出管程(Monitor)对象来实现同步。</font></b><br><br>但 synchronized 关键字实现同步代码块和同步方法的细节不一样,<br><b>代码块同步是使用 monitorenter 和 monitorexit 指令实现的</b>,<br><b>方法同步通过调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的</b>。
内存锁
Java语法规定,任何线程执行同步方法、同步代码块 之前,必须先获取对应的<b>监视器</b>。<br>JAVA的synchronized()方法类似于操作系统概念中的互斥内存块,<br><b>在JAVA中的Object类型中,都是带有一个内存锁的,在有线程获取该内存锁后,其它线程无法访问该内存</b>,<br>从而实现JAVA中简单的同步、互斥操作。
锁住代码块时的底层原理?(类锁)
public class SynTest{<br> public int i;<br><br> public void syncTask(){<br><b><font color="#f15a23"> synchronized (this){<br> i++;<br> }</font></b><br> }<br>}<br>
反编译后
锁住方法时的底层原理?(对象锁)
public class SynTest{<br> public int i;<br><br><b><font color="#f15a23"> public synchronized void syncTask(){<br> i++;<br> }</font></b><br>}
反编译后
jvm 可以从方法常量池中的方法表结构的 ACC_SYNCHRONIZED 的标记区分一个方法是否需要加锁;<br><br>当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,<br>如果设置了,执行线程将先持有 monitor, 然后再执行方法,<br>最后在方法完成(无论是正常完成还是非正常完成)时释放 monitor。<br>
<b><font color="#c41230">java 对象头:</font></b><br>Java对象头是synchronized实现的关键,<br>synchronized用的锁是存在Java对象头里的。<br>
<b><font color="#f15a23">一个 java 对象包含哪些东西?</font></b><br><br>在JVM中,对象在内存中的布局分为三块区域:<b>对象头、实例数据 ,对齐填充</b>。<br>
<b><font color="#f15a23">对象头和 Markword 的关系?</font></b><br><br>对象头中有一个Mark Word 和 Class Metadata Address 组成;<br>
<b><font color="#f15a23">Mark Word 和 Sychronized 的关系?</font></b><br><br>Mark Word 存储了 Sychronized 锁的标注位信息
Sychronized 锁的四种状态:
<b><font color="#f15a23">无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态</font></b>,<br>这几种状态会随着竞争情况逐渐升级,但不能降级,目的是为了提高锁和释放锁的效率。
Lock
AQS-- AbstractQueuedSynchronizer
Lock
读写锁 - ReentrantReadWriteLock
可重入锁 - ReetrantLock 实现了Lock接口
lock( )方法
是平常使用得最多的一个方法,就是用来获取锁。如果锁已被其他线程获取,则进行等待。<br>如果采用Lock,必须主动去释放锁,并且在发生异常时,不会自动释放锁。<br>因此一般来说,使用Lock必须在try{ }catch{ }块中进行,并且将释放锁的操作放在finally块中进行,<br>以保证锁一定被被释放,防止死锁的发生。<br>
tryLock( ) 方法
方法是有返回值的,它表示用来尝试获取锁,如果获取成功,则返回true,<br>如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。<br>在拿不到锁时不会一直在那等待。
tryLock(long time, TimeUnit unit)方法
方法和tryLock()方法是类似的,只不过区别在于这个方法在拿不到锁时会等待一定的时间,<br>在时间期限之内如果还拿不到锁,就返回false。如果一开始拿到锁或者在等待期间内拿到了锁,则返回true。
lockInterruptibly( ) 方法
unLock( )方法
与锁相关的几个概念
1.可重入锁
2. 可中断锁
3.公平锁
4.非公平锁
5. 读写锁
AQS图解
AQS就是一个并发包的基础组件,用来实现各种锁,各种同步组件的。<br>它包含了<b>state变量</b>、<b>加锁线程</b>、<b>等待队列,Condition</b>等并发中的核心组件。
数据结构
state变量
表示加锁的状态。
使用CAS 把 state 由 0 -> 1
加锁线程
表示当前加锁的线程。
Condition
Condition<br><br>synchronized与wait()和nitofy()/notifyAll()方法相结合可以实现等待/通知模型,ReentrantLock同样可以,但是需要借助Condition,且Condition有更好的灵活性,具体体现在:<br><br>1、一个Lock里面可以创建多个Condition实例,实现多路通知<br><br>2、notify()方法进行通知时,被通知的线程是Java虚拟机随机选择的,但是ReentrantLock结合Condition可以实现有选择性地通知,这是非常重要的
wait
阻塞
singnal
唤醒
signalAll
我们要打印1到9这9个数字,由A线程先打印1,2,3,然后由B线程打印4,5,6,然后再由A线程打印7,8,9. 这道题有很多种解法,现在我们使用Condition来做这道题?
//第一个条件当屏幕上输出到3<br> final Condition reachThreeCondition = lock.newCondition();<br> //第二个条件当屏幕上输出到6<br> final Condition reachSixCondition = lock.newCondition();
AQS lock 操作流程图
1,线程A调用lock 方法,先CAS 设置 state 变量
成功:执行逻辑,执行完,state--,唤醒AQS中队列的head 节点的线程(如果有的话)
失败:说明已经有线程获取了锁,判断正在加锁的线程是否是自己
是:state++
否:构建 Node 节点把自己添加到阻塞队列。(使用CAS的方法)
然后判断自己是不是head,是的话就CAS 尝试获取锁,失败的话就阻塞等待唤醒。
AQS 和 ReentrantLock 的关系?
类继承关系
类解释
AQS 提供了基本的加锁释放锁的方法,还有Condition等功能;<br>ReentrantLock 在内部类中扩展了AQS 的功能,实现了公平锁和非公平锁。
ReentrantLock内部包含了一个AQS对象,也就是AbstractQueuedSynchronizer类型的对象。<br>这个AQS对象就是ReentrantLock可以实现加锁和释放锁的关键性的核心组件。
AQS 使用了模板方法设计模式<br>使用的时候,需要继承 AbbstractQueuedSychronized 并重写指定的方法;<br>通常是将子类定义为同步组件的静态内部类。
公平锁与非公平锁 (ReentrantLock 构造函数可以选择使用公平锁和非公平锁)
ReentrantLock 内部基于 AQS 对象 实现了公平锁和非公平锁
<b><font color="#f15a23">非公平锁</font></b>: 不用判断AQS的等待队列中有没有正在等待加锁的线程,直接对state变量进行CAS操作
<b><font color="#f15a23">公平锁</font></b>:需要先判断AQS的等待队列中有没有等待加锁的线程,有就把自己添加到等待队列中中,没有在进行CAS进行加锁操作
Synchroized 和 Lock 对比
有了Synchroized为什么还要Lock?
Lock提供了比Synchroized更多的功能,其中使用上需要注意:
简述synchronized和java.util.concurrent.locks.Lock的异同 ?
synchroized<br>(synchroized现在已经得到了很大的优化,不会比lock差太多,锁会自动升级)
作用对象
修饰实例方法
修饰静态方法
修饰代码块
<b>JDK1.6 对锁的实现引入了大量的优化</b>,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。<br>锁主要存在四中状态,依次是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态,他们会随着竞争的激烈而逐渐升级。注意锁可以升级不可降级,这种策略是为了提高获得锁和释放锁的效率。
volatile
总线风暴问题?
多核处理器中,所有 CPU 共用一条数据总线,CPU 缓存都是靠总线和主内存进行数据交互.<br>当主内存存在于多个 CPU 缓存中,而其中一个 CPU 更新的缓存,就会触发嗅探机制通过总线通知所有的 CPU,<br>上面这种方式称为<b><font color="#f15a23">缓存一致性流量</font></b>
java 中的CAS 底层是使用由 cpp 使用汇编实现, 底层使用 CPU 的 Lock 指令,如果短时间内有大量的 CAS 操作 加上 volatile 的嗅探机制,就会不断地向总线宽带发送消息,导致总线宽带激增,产生<b><font color="#f15a23">总线风暴</font></b>.
可见性<br>有序性
volatile 能使得一个非原子操作变成原子操作吗?
能!<br>回答这个之前要先说一下 volatile 的作用:内存可见性,禁止指令重排。并没有保证原子性的功能。<br>但是对 两个 java 的基本数据类型 long 和 double 例外,因为 long 类型的变量读取不是原子的,<br>先读取前 32 位,再读取后 32 位。但是如果被 volatile 修饰,那就是可以保证原子操作
volatile 修饰符使用实践?<br>volatile 怎么实现内存可见性的?-- 内存屏障
图
死锁,活锁,饥饿 是什么以及它们的区别?
死锁
指两个或两个以上线程在执行中,因争夺资源造成的一种互相等待的现象。
活锁
由于某些条件不满足,导致一直重复尝试,失败,尝试,失败。。。
死锁与活锁的区别?
饥饿
一个或者多个线程由于无法获取所需资源,导致一直无法执行的状态。
导致线程饥饿的原因?
抢占式调度+线程优先级
当一个线程进入一个对象的 sychronized 修饰的实例方法后,<br>其它线程是否还可以进入其它非 sychronized 修饰的方法?
可以。如果其他方法没有 synchronized的话,其他线程是可以进入的<br>所以要开放一个线程安全的对象时,得保证每个方法都是线程安全的
一个线程在访问一个对象的同步方法时,另一个线程可以同时访问这个对象的非同步方法。
一个线程在访问一个对象的同步方法时,另一个线程不能同时访问这个同步方法。
一个线程在访问一个对象的同步方法时,另一个线程不能同时访问这个对象的另一个同步方法。
多线程
进程与线程的区别?
进程:是操作系统<font color="#f15a23">资源分配</font>的最小单元。<br>线程: 是操作系统<font color="#f15a23">资源调度</font>的最小单元。<br>一个程序至少有一个进程,一个进程至少有一个线程
进程间的几种通信方式说一下?
线程间的通信方式?
线程间的通信主要用来做线程同步
java 中有几种方法可以实现一个线程?
继承 Thread 类 --> run()
实现 Runnable 接口 ---> run()
实现 Callable 接口 --> call(),推荐,可以在线程结束时在 call 方法中返回值
什么是 Callable 和 Future 和 FutureTask?
Callable 被线程执行后用来产生结果,Future 用来获取结果。
FutureTask
Thread 的生命周期?
java的线程与通用线程生命周期?
JAVA的线程生命周期模型在操作系统的基础上稍作了修改,<br>一是Runnable表示可运行与运行状态(等待资源依然是Runnable),<br>二是将休眠状态扩展成了Blocked&Waiting&Time_waiting三种状态。
操作系统的五个状态?
初始状态(New)
当是使用 new 创建一个线程后,线程处于新建状态,<br>此时由JVM为其分配内存,并初始化成员变量的值。
就绪状态(Runnable)
当线程调用了 start() 方法后,就处于 就处于 Runnable 状态,<br>JVM会为其创建方法调用栈和程序计数器,等待调度运行。<br>
完事具备只欠CPU
运行状态(Running)-java中不存在
处于 Runnable 状态的线程获得CPU,开始执行 run() 方法,该线程处于 Running 状态。<br>Running状态的线程如果调用一个阻塞的 API(例如以阻塞方式读文件)或者等待某个事件(例如条件变量比如 sychronized 锁),那么线程的状态就会转换到休眠状态,同时释放 CPU 使用权,休眠状态的线程永远没有机会获得 CPU 使用权。当等待的事件出现了,线程就会从休眠状态转换到Runnable状态。<br>
休眠状态(Blocked,Waiting,TimedWaiting)
当处于 Running 状态的线程失去CPU,变进入 休眠 状态。
终止状态(TERMINATED)
stop(),interrupt() 线程执行完毕,或者异常退出。该线程结束生命周期。
stop() 会强制杀死这个线程,可能导致 Lock 锁不会释放,导致死锁发生,不推荐使用。
如何停止一个正在运行的线程?
一,使用共享变量 boolean 标记
二,使用 Thread.interrupt() 方法,该方法虽然不会中断一个正在运行的线程,<br> 但是它可以使一个被阻塞的线程抛出一个中断异常,从而使线程提前结束阻塞状态。
多线程协调(看链接,讲的非常好)
CountDownLatch(计时器)
运动员起跑程序
构造函数设置计数器起始值,countDown() 计数器减一,当计数器减到0 时,readyLatch.await() 不再阻塞。
CyclicBarrier(循环屏障)
适用于以下场景:
各个线程都必须完成某项任务(写数据)才能继续做后续的任务;<br>各个线程需要相互等待,不能独善其身。
运动员准备起跑程序
CyclicBarrier barrier = new CyclicBarrier(5, new Runnable() {<br> public void run() {<br> System.out.println(Thread.currentThread().getId() + ":我宣布,所有小伙伴写入数据完毕");<br> }<br> });<br><br>barrier 有5个参与者,只有 5个参与者(线程)可以拿到 barrier 调用其 await() 方法,进行等待,<br>barrier 被调用了 5次,就会执行后面的 runnabble .<br><br>当然也可以给 await() 方法设置超时时间,超时抛出异常。
在java 多线程编程中你有那些最佳实践?
a)给线程命名,这样可以帮助调试。<br>b)最小化同步的范围,而不是将整个方法同步,只对关键部分做同步<br>c)如果可以,更偏向于使用 volatile而不是 synchronized。<br>d)<b><font color="#f1753f">使用更高层次的并发工具,而不是使用 waito和 notify()来实现线程间<br>通信,如 Blockingqueue, Countdownlatch及 Semaphore</font></b><br>e)优先使用并发集合,而不是对集合进行同步。并发集合提供更好的可扩展性。<br>f)将线程和任务分离,使用线程池执行 Runnable 和 Callable
什么是 CopyOnWriteArrayList?可用于什么场景?
免锁容器。好处是当有多个线程同事遍历修改这个 列表时,不会发生 <br>ConcurrentModificationException 异常。
在 CopyOnWriteArrayList 中,写入操作会导致创建整个底层数组的副本,原数组会保留在原地。<br>是的副本数组在被修改时,读取操作可以安全执行。
缺点:
由于写操作会拷贝数组,消耗内存,如果数组过大,可能会导致频繁 Yong GC 或者 Full GC
不能用于实时读的场景,拷贝数组需要时间,所以调用 set 操作后,读取的值可能还是旧的。<br>虽然 CopyOnWriteArrayList 可以做到最终一致性,但无法保证实时性。
设计思想:
读写分离。
使用额外空间的思路,解决并发冲突。
最终一致性。
线程池
为什么使用 Executor 框架?
直接 new Thread 的缺点?
耗费资源
不能控制线程数量
一些特殊功能比如定时执行的线程,不方便
Executor 框架的优点?
复用线程
控制线程数量
使用简单
Executor 和 Executors 的区别?
ForkJoin 框架?
线程池?
<b><font color="#f15a23">任务调度流程图</font></b>
线程池底层原理?
所有的任务都在workQueue 中, workerQueue 是 BlockingQueue 的实现类实现,Worker 实现了 Runnable 接口,在run方法中维持一个循环判断队列是否有任务需要执行<br>ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue)<br>
线程池的 submit() 和 execute() 方法有什么区别?
两个方法都可以向线程池中提交任务。
<b><font color="#f15a23">submit()</font></b> 方法定义在 Executor 接口中,返回类型是 void
<b><font color="#f15a23">execute()</font></b> 方法定义在 ExecutorService 接口中,返回持有运算结果的 Future 对象。
ThreadPoolExecutor
FixedThreadPool(固定线程数线程池)
CachedThreadPool(可缓存的线程池)
SingleThreadExecutor(单线程池:创建一个核心线程)
ScheduledThreadPool(固定数量,支持定时,和周期的线程)
DelayQueue 实现
实现Runnable接口和Callable接口的区别?
执行execute()方法和submit()方法的区别是什么呢?
Java线程池有哪些参数?阻塞队列有几种?拒绝策略有几种?
线程池的 6 个参数<br>四种拒绝策略<br>三种阻塞队列
<b><font color="#f15a23">corePoolSize</font></b>: 线程池维护线程的最少数量<br><br><b><font color="#f15a23">maximumPoolSize</font></b>:线程池维护线程的最大数量<br><br><b><font color="#f15a23">keepAliveTime</font></b>: 线程池维护线程所允许的空闲时间<br><br>unit: 线程池维护线程所允许的空闲时间的单位<br><br><b><font color="#f15a23">workQueue</font></b>: 线程池所使用的缓冲队列(三种)<br><br>handler: 线程池对拒绝任务的处理策略(四种)<br>
流程
四种拒绝策略
四种拒绝策略:<br> RejectedExecutionHandler rejected = null;<br> rejected = new ThreadPoolExecutor.AbortPolicy();//默认,队列满了丢任务抛出异常<br> rejected = new ThreadPoolExecutor.DiscardPolicy();//队列满了丢任务不异常<br> rejected = new ThreadPoolExecutor.DiscardOldestPolicy();//将最早进入队列的任务删,之后再尝试加入队列<br> rejected = new ThreadPoolExecutor.CallerRunsPolicy();//如果添加到线程池失败,那么主线程会自己去执行该任务
三种阻塞队列
ArrayBlockingQueue
有界
基于数组的先进先出
LinkedBlockQueue
无界
基于链表的先进先出
SynchronizedQueue
无界
无缓冲的等待队列
什么是阻塞队列(BlockingQueue)?
Block ingQueue 是一个支持两个附加操作的队列接口。<br>* 在队列为空时,获取元素的线程会等待队列有值;<br>* 在队列已满时,存储元素的线程会等待队列可用。
有很多实现类
JDK7提供了7个阻塞队列。分别是<br>Arrayblockingqueue :一个由数组结构组成的有界阻塞队列<br>LinkedBlockingQueue:由链表结构组成的有界阻塞队列<br>PriorityblockingQueue:一个支持优先级排序的无界阻塞队列<br>De layQueue:一个使用优先级队列实现的无界阻塞队列<br>SynchronousQueue:一个不存储元素的阻塞队列。<br>Linkedtransfer@ueue:一个由链表结构组成的无界阻塞队列<br>LinkedBlockingDeque:一个由链表结构组成的双向阻塞队列。
take() 方法没有值的时候会阻塞,阻塞使用的是 Conidtion 的await()方法实现的。
/** Main lock guarding all access */<br> final ReentrantLock lock;<br><br> /** Condition for waiting takes */<br> private final<b><font color="#f15a23"> Condition notEmpty</font></b>;<br><br> /** Condition for waiting puts */<br> private final Condition notFull; <br><br><br> public E take() throws InterruptedException {<br> final ReentrantLock lock = this.lock;<br> lock.lockInterruptibly();<br> try {<br> while (count == 0)<br><b><font color="#f15a23"> notEmpty.await();</font></b><br> return dequeue();<br> } finally {<br> lock.unlock();<br> }<br> }
Queue
定义的方法说明
0 条评论
下一页